From 4d9916882eba53bcad537114aaa6cdb7552c1b58 Mon Sep 17 00:00:00 2001 From: Harold Jin Date: Sat, 30 Aug 2025 17:27:44 -0400 Subject: [PATCH 01/10] feat: support multi-location --- .flake8 | 1 + .vscode/launch.json | 91 +++++ .vscode/settings.json | 44 ++ DEVELOPMENT_SETUP.md | 376 ++++++++++++++++++ MULTIPLE_ACCOUNTS_UPDATE.md | 170 ++++++++ custom_components/sunpower/__init__.py | 44 +- custom_components/sunpower/binary_sensor.py | 9 +- custom_components/sunpower/config_flow.py | 31 +- custom_components/sunpower/const.py | 1 + custom_components/sunpower/entity.py | 16 +- custom_components/sunpower/sensor.py | 9 +- custom_components/sunpower/strings.json | 3 +- .../sunpower/translations/en.json | 59 +-- env.example | 18 + requirements-dev.txt | 22 + testing/DEBUG_TOOLS_GUIDE.md | 306 ++++++++++++++ testing/README.md | 150 +++++++ testing/debug_runner.py | 120 ++++++ {samples => testing/samples}/device_list.json | 0 testing/test_api.py | 82 ++++ testing/test_ha_integration.py | 143 +++++++ testing/test_with_sample_data.py | 190 +++++++++ tests/__init__.py | 1 + tests/test_sunpower_api.py | 74 ++++ 24 files changed, 1904 insertions(+), 56 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 DEVELOPMENT_SETUP.md create mode 100644 MULTIPLE_ACCOUNTS_UPDATE.md create mode 100644 env.example create mode 100644 requirements-dev.txt create mode 100644 testing/DEBUG_TOOLS_GUIDE.md create mode 100644 testing/README.md create mode 100755 testing/debug_runner.py rename {samples => testing/samples}/device_list.json (100%) create mode 100755 testing/test_api.py create mode 100755 testing/test_ha_integration.py create mode 100755 testing/test_with_sample_data.py create mode 100644 tests/__init__.py create mode 100644 tests/test_sunpower_api.py diff --git a/.flake8 b/.flake8 index d68af71..d373378 100644 --- a/.flake8 +++ b/.flake8 @@ -19,6 +19,7 @@ exclude = docker-compose.yaml, frontend, *.md, + */*.md, *.txt extend-select = diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..14137f2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,91 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug SunPower Integration", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/testing/debug_runner.py", + "console": "integratedTerminal", + "envFile": "${workspaceFolder}/.env", + "env": { + "PYTHONPATH": "${workspaceFolder}:${workspaceFolder}/custom_components" + }, + "cwd": "${workspaceFolder}", + "justMyCode": false, + "stopOnEntry": false + }, + { + "name": "Test with Sample Data", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/testing/test_with_sample_data.py", + "console": "integratedTerminal", + "env": { + "PYTHONPATH": "${workspaceFolder}:${workspaceFolder}/custom_components" + }, + "cwd": "${workspaceFolder}", + "justMyCode": false, + "stopOnEntry": false + }, + { + "name": "Debug HA Integration", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/testing/test_ha_integration.py", + "console": "integratedTerminal", + "envFile": "${workspaceFolder}/.env", + "env": { + "PYTHONPATH": "${workspaceFolder}:${workspaceFolder}/custom_components" + }, + "cwd": "${workspaceFolder}", + "justMyCode": false, + "stopOnEntry": false + }, + { + "name": "Debug SunPower API Client", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/testing/test_api.py", + "console": "integratedTerminal", + "envFile": "${workspaceFolder}/.env", + "env": { + "PYTHONPATH": "${workspaceFolder}:${workspaceFolder}/custom_components" + }, + "cwd": "${workspaceFolder}", + "justMyCode": false, + "stopOnEntry": false + }, + { + "name": "Debug Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "envFile": "${workspaceFolder}/.env", + "env": { + "PYTHONPATH": "${workspaceFolder}:${workspaceFolder}/custom_components" + }, + "cwd": "${workspaceFolder}", + "justMyCode": false + }, + { + "name": "Run Tests", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": [ + "tests/", + "-v", + "--tb=short" + ], + "console": "integratedTerminal", + "envFile": "${workspaceFolder}/.env", + "env": { + "PYTHONPATH": "${workspaceFolder}:${workspaceFolder}/custom_components" + }, + "cwd": "${workspaceFolder}", + "justMyCode": false + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5bf4f2f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,44 @@ +{ + "python.defaultInterpreterPath": "./venv/bin/python", + "python.terminal.activateEnvironment": true, + "python.linting.enabled": true, + "python.linting.flake8Enabled": true, + "python.linting.pylintEnabled": false, + "python.formatting.provider": "black", + "python.formatting.blackArgs": [ + "--line-length", + "99" + ], + "python.sortImports.args": [ + "--profile", + "black" + ], + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "files.exclude": { + "**/__pycache__": true, + "**/*.pyc": true, + ".pytest_cache": true, + ".coverage": true, + "htmlcov": true + }, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } + }, + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestArgs": [ + "tests" + ], + "python.analysis.typeCheckingMode": "basic", + "python.analysis.autoImportCompletions": true, + "python.analysis.extraPaths": [ + "./custom_components" + ] +} diff --git a/DEVELOPMENT_SETUP.md b/DEVELOPMENT_SETUP.md new file mode 100644 index 0000000..de4b456 --- /dev/null +++ b/DEVELOPMENT_SETUP.md @@ -0,0 +1,376 @@ +# SunPower Integration Development Setup + +This guide will help you set up a complete development environment for the SunPower Home Assistant integration, including debugging capabilities and import stepping. + +## ๐Ÿ—๏ธ **Project Overview** + +This is a **Home Assistant custom integration** for monitoring SunPower solar systems via local PVS (Photovoltaic Supervisor) interface. The integration provides real-time data for: + +- Solar panel production (per-panel data) +- Power consumption and grid interaction +- Battery storage systems (SunVault) +- System health and diagnostics + +## ๐Ÿ“‹ **Prerequisites** + +- **Python 3.11+** (tested with 3.13.5) +- **VS Code** (recommended) or PyCharm +- **Git** for version control +- Access to a **SunPower PVS system** (for testing) + +## ๐Ÿš€ **Quick Setup** + +### 1. **Clone and Enter Project** + +```bash +cd /path/to/your/projects +git clone https://github.com/krbaker/hass-sunpower.git +cd hass-sunpower +``` + +### 2. **Create Virtual Environment** + +```bash +python3 -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +``` + +### 3. **Install Dependencies** + +```bash +pip install --upgrade pip +pip install -r requirements-dev.txt +``` + +### 4. **Setup Pre-commit Hooks** + +```bash +pre-commit install +``` + +### 5. **Configure Environment** + +```bash +cp env.example .env +# Edit .env with your PVS IP address +``` + +## ๐Ÿ”ง **VS Code Setup** + +The project includes pre-configured VS Code settings for optimal development: + +### **Extensions (Install these):** + +- Python (ms-python.python) +- Pylance (ms-python.vscode-pylance) +- Black Formatter (ms-python.black-formatter) +- isort (ms-python.isort) +- GitLens (eamodio.gitlens) + +### **Key Features Configured:** + +- โœ… **Auto-formatting** with Black (line length 99) +- โœ… **Import sorting** with isort +- โœ… **Linting** with Flake8 +- โœ… **Type checking** with Pylance +- โœ… **Auto-save formatting** +- โœ… **Debugging configurations** + +## ๐Ÿ› **Debugging Setup** + +### **Debug Configurations Available:** + +1. **Debug SunPower Integration** - Test core functionality (no HA required) +2. **Test with Sample Data** - Test integration logic with sample data +3. **Debug HA Integration** - Test Home Assistant coordinator setup +4. **Debug SunPower API Client** - Test just the API client +5. **Debug Current File** - Debug any Python file +6. **Run Tests** - Debug test cases + +### **Setting Breakpoints:** + +1. **Open any Python file** in the integration +2. **Click in the gutter** (left of line numbers) to set breakpoints +3. **Press F5** or use Debug menu to start debugging +4. **Step through code** with F10 (step over), F11 (step into) + +### **Debug Scripts:** + +#### **Test API Connection:** + +```bash +# Set your PVS IP +export PVS_HOST=192.168.1.100 # Replace with your PVS IP +python test_api.py +``` + +Example Output + +```bash +โžœ hass-sunpower git:(main) โœ— ./venv/bin/python3 test_api.py +Testing SunPower API connection to young-ave.dynamic-dns.net:8081 +-------------------------------------------------- +1. Testing network status... +โœ… Network status successful +{'networkstatus': {'interfaces': [{'interface': 'wan', + 'internet': 'down', + 'ipaddr': '', + 'link': 'disconnected', + 'mode': 'wan', + 'sms': 'unreachable', + 'state': 'down'}, + {'interface': 'plc', + 'internet': 'down', + 'ipaddr': '', + 'link': 'disconnected', + 'pairing': 'unpaired', + 'sms': 'unreachable', + 'speed': 0, + 'state': 'down'}, + {'interface': 'sta0', + 'internet': 'up', + 'ipaddr': '192.168.1.25', + 'signal': '-80', + 'sms': 'reachable', + 'ssid': "XYZ's Network", + 'status': 'connected'}, + {'interface': 'cell', + 'internet': 'down', + 'ipaddr': '', + 'is_alwayson': False, + 'is_primary': False, + 'link': 'disconnected', + 'modem': 'MODEM_OK', + 'provider': 'UNKNOWN', + 'signal': 0, + 'sim': 'SIM_READY', + 'sms': 'unreachable', + 'state': 'DOWN', + 'status': 'NOT_REGISTERED'}], + 'system': {'interface': 'sta0', + 'interface_name': 'sta0', + 'internet': 'up', + 'sms': 'reachable'}, + 'ts': '1756569304'}, + 'result': 'succeed'} + +2. Testing device list... +โœ… Device list successful +Found 30 devices +Device breakdown: + - PVS: 1 + - Power Meter: 2 + - Inverter: 27 + +3. Testing energy storage system status... +โœ… ESS status successful +{'result': 'Make sure you have run discovery to successful completion'} +``` + +#### **Test with Sample Data (No PVS Required):** + +```bash +python test_with_sample_data.py +``` + +#### **Debug Integration Logic:** + +```bash +export PVS_HOST=192.168.1.100 +python debug_runner.py +``` + +#### **Test Home Assistant Integration:** + +```bash +export PVS_HOST=192.168.1.100 +python test_ha_integration.py +``` + +## ๐Ÿงช **Testing** + +### **Run All Tests:** + +```bash +pytest tests/ -v +``` + +### **Run Specific Test:** + +```bash +pytest tests/test_sunpower_api.py::TestSunPowerMonitor::test_init -v +``` + +### **Run Tests with Coverage:** + +```bash +pytest tests/ --cov=custom_components/sunpower --cov-report=html +``` + +## ๐Ÿ” **Code Quality Tools** + +### **Manual Code Checks:** + +```bash +# Format code +black custom_components/ tests/ *.py + +# Sort imports +isort custom_components/ tests/ *.py + +# Lint code +flake8 custom_components/ tests/ *.py + +# Type checking +mypy custom_components/sunpower/ +``` + +### **Pre-commit (Automatic):** + +Code quality checks run automatically on commit. To run manually: + +```bash +pre-commit run --all-files +``` + +## ๐Ÿ“ **Key Files for Development** + +### **Core Integration Files:** + +- `custom_components/sunpower/__init__.py` - Integration entry point +- `custom_components/sunpower/sunpower.py` - API client +- `custom_components/sunpower/const.py` - Constants and sensor definitions +- `custom_components/sunpower/config_flow.py` - UI configuration +- `custom_components/sunpower/sensor.py` - Sensor entities +- `custom_components/sunpower/entity.py` - Base entity class + +### **Development Files:** + +- `test_api.py` - Test PVS API connection +- `debug_runner.py` - Debug full integration +- `requirements-dev.txt` - Development dependencies +- `.vscode/launch.json` - Debug configurations +- `tests/` - Test suite + +## ๐ŸŒ **Network Setup for Testing** + +The integration connects to the PVS management interface: + +### **Common PVS IP Addresses:** + +- **NAT setup**: `172.27.153.1` (most common) +- **Direct connection**: Your PVS's actual IP +- **Router assignment**: Check your router's DHCP clients + +### **Testing Connectivity:** + +```bash +# Test if PVS is reachable +curl "http://172.27.153.1/cgi-bin/dl_cgi?Command=Get_Comm" + +# Or use our test script +export PVS_HOST=172.27.153.1 +python test_api.py +``` + +## ๐Ÿšจ **Important Notes** + +### **PVS Management Interface:** + +- โš ๏ธ **DO NOT** plug the PVS management port directly into your LAN +- It runs its own DHCP server and will cause network issues +- Use a separate network interface or NAT setup + +### **Development Safety:** + +- Use longer polling intervals (120s+) to avoid overwhelming the PVS +- The PVS API is slow and can timeout - be patient +- Monitor PVS logs for any issues during development + +## ๐Ÿ”ง **Stepping Through Imports** + +### **Debug Import Issues:** + +1. **Set breakpoint** in `__init__.py` at the import statements +2. **Start debug session** "Debug SunPower Integration" +3. **Step through** (F11) each import to see what's loaded +4. **Check sys.path** in debug console: `sys.path` +5. **Verify module loading** in debug console: `import custom_components.sunpower.const` + +### **Import Path Configuration:** + +```python +# VS Code settings already include: +"python.analysis.extraPaths": ["./custom_components"] + +# Debug scripts add: +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'custom_components')) +``` + +## ๐Ÿ“ **Common Development Tasks** + +### **Adding a New Sensor:** + +1. **Add sensor definition** in `const.py` +2. **Test data availability** in sample JSON +3. **Add unit tests** in `tests/` +4. **Test with real PVS** using debug scripts + +### **Debugging Connection Issues:** + +1. **Check network connectivity**: `python test_api.py` +2. **Verify PVS responds**: `curl http:///cgi-bin/dl_cgi?Command=Get_Comm` +3. **Debug data parsing**: Set breakpoints in `sunpower_fetch()` +4. **Check coordinator updates**: Debug in `async_update_data()` + +### **Adding Multi-Account Support:** + +The integration supports multiple PVS systems: + +1. **Use unique names** in config flow +2. **Test entry_id isolation** in debug scripts +3. **Verify device/entity separation** in Home Assistant + +## ๐Ÿ†˜ **Troubleshooting** + +### **Import Errors:** + +```bash +# Check Python path +python -c "import sys; print('\n'.join(sys.path))" + +# Verify module structure +find custom_components/ -name "*.py" | head -10 +``` + +### **VS Code Issues:** + +- **Restart Python interpreter**: Cmd+Shift+P โ†’ "Python: Restart Language Server" +- **Check interpreter**: Cmd+Shift+P โ†’ "Python: Select Interpreter" โ†’ Choose `./venv/bin/python` +- **Reload window**: Cmd+Shift+P โ†’ "Developer: Reload Window" + +### **Debug Not Working:** + +1. **Check .env file** exists with PVS_HOST +2. **Verify PVS connectivity** with `test_api.py` +3. **Check Python interpreter** points to `./venv/bin/python` +4. **Look at debug console** for error messages + +## ๐ŸŽฏ **Next Steps** + +1. **Test with your PVS**: Update `.env` with your PVS IP +2. **Run debug script**: `python debug_runner.py` +3. **Set breakpoints**: Try debugging the data flow +4. **Add tests**: Create tests for any new features +5. **Submit PRs**: Follow the project's contribution guidelines + +--- + +## ๐Ÿ“š **Additional Resources** + +- **Home Assistant Developer Docs**: +- **SunPower Integration Issues**: +- **Home Assistant Discord**: #devs_custom_components +- **Python Debugging Guide**: + +Happy coding! ๐Ÿš€ diff --git a/MULTIPLE_ACCOUNTS_UPDATE.md b/MULTIPLE_ACCOUNTS_UPDATE.md new file mode 100644 index 0000000..1146a0a --- /dev/null +++ b/MULTIPLE_ACCOUNTS_UPDATE.md @@ -0,0 +1,170 @@ +# Multiple Accounts/Locations Support Update + +## Overview + +This update enables the SunPower Home Assistant integration to support multiple accounts and locations without sensor or device conflicts. Each account/location installation is now properly isolated with unique identifiers. + +## Changes Made + +### 1. Config Flow Updates (`config_flow.py`) + +- Added optional `name` field for location identification +- Updated unique_id generation to include location name when provided +- Enhanced validation and error handling +- Updated UI strings and translations + +### 2. Core Architecture Changes (`__init__.py`) + +- **Fixed global data storage issue**: Replaced global variables with entry-specific cache (`ENTRY_DATA_CACHE`) +- Updated `sunpower_fetch()` to accept and use `entry_id` parameter +- Added proper cleanup in `async_unload_entry()` to prevent memory leaks +- Enhanced data isolation between multiple installations + +### 3. Entity Identification System (`entity.py`, `sensor.py`, `binary_sensor.py`) + +- **Enhanced unique_id generation**: All entities now include entry_id in their unique_id to prevent conflicts +- **Updated device identifiers**: Device identifiers now include entry_id to ensure device uniqueness +- **Backwards compatibility**: System falls back to old format when entry_id is not available +- Added entry_id parameter to all entity constructors + +### 4. Translation Updates + +- Updated `strings.json` and `translations/en.json` with new location name field +- Enhanced descriptions to guide users on multiple account setup + +## Key Features + +### Conflict Prevention + +- **Entity unique_id format**: `{entry_id}_{device_serial}_pvs_{field}` (vs old: `{device_serial}_pvs_{field}`) +- **Device identifiers**: `(domain, "{entry_id}_{device_serial}")` (vs old: `(domain, "{device_serial}")`) +- **Config entry unique_id**: `{host}_{location_name}` (vs old: `{host}`) + +### Data Isolation + +- Each account/location has its own data cache +- No shared global variables between installations +- Proper cleanup on entry removal + +### User Experience + +- Optional location name field for easy identification +- Clear UI guidance for multiple account setup +- Backwards compatible with existing installations + +## Testing Plan + +### 1. Single Account (Backwards Compatibility) + +1. Install integration without location name +2. Verify all sensors and devices work as before +3. Check entity unique_ids maintain backwards compatibility + +### 2. Multiple Accounts Setup + +1. **First Account**: + - Configure with Host: `192.168.1.100` and Name: `Home Main` + - Verify all entities are created with format: `{entry_id}_...` + +2. **Second Account**: + - Configure with Host: `192.168.1.101` and Name: `Cabin Solar` + - Verify no conflicts with first account + - Check that devices and entities are properly separated + +3. **Same Host, Different Names**: + - Configure with Host: `192.168.1.100` and Name: `Home Backup` + - Verify this creates a separate integration instance + - Confirm no entity or device conflicts + +### 3. Conflict Validation + +1. Check Home Assistant's Developer Tools โ†’ States +2. Verify no duplicate entity IDs exist +3. Confirm device registry shows separate devices for each account +4. Test that data updates work independently for each account + +### 4. Cleanup Testing + +1. Remove one integration instance +2. Verify the cache is properly cleaned up +3. Confirm other instances continue working normally +4. Check for memory leaks in logs + +## Migration Notes + +### For Existing Users + +- **No action required**: Existing installations will continue to work unchanged +- Entity unique_ids remain the same for backwards compatibility +- Devices maintain their current identifiers + +### For New Multi-Account Users + +1. When adding a second account, **always use the location name field** +2. Use descriptive names like "Main House", "Cabin", "Garage", etc. +3. Each location should have a unique combination of host and name + +## Technical Details + +### Entry-Specific Data Cache + +```python +ENTRY_DATA_CACHE = { + "entry_id_1": { + "pvs_sample_time": 0, + "pvs_sample": {}, + "ess_sample_time": 0, + "ess_sample": {} + }, + "entry_id_2": { + # ... separate cache for another account + } +} +``` + +### Entity Unique ID Examples + +```python +# Old format (still used for backwards compatibility) +"E00202040011392_pvs_ltea_3phsum_kwh" + +# New format with entry_id +"abc123def456_E00202040011392_pvs_ltea_3phsum_kwh" +``` + +### Device Identifier Examples + +```python +# Old format +("sunpower", "E00202040011392") + +# New format +("sunpower", "abc123def456_E00202040011392") +``` + +## Troubleshooting + +### If You See Entity Conflicts + +1. Ensure each account uses a unique location name +2. Restart Home Assistant after configuration changes +3. Check Developer Tools โ†’ States for duplicate entities + +### Performance Considerations + +- Each account maintains its own update intervals +- Data caching is isolated per account to prevent interference +- Memory usage scales linearly with number of accounts + +## Future Enhancements + +Potential future improvements: + +1. Bulk account configuration +2. Account grouping in UI +3. Cross-account energy summary +4. Advanced conflict detection and resolution + +## Breaking Changes + +**None** - This update is fully backwards compatible with existing installations. diff --git a/custom_components/sunpower/__init__.py b/custom_components/sunpower/__init__.py index ba16edc..1e51d35 100644 --- a/custom_components/sunpower/__init__.py +++ b/custom_components/sunpower/__init__.py @@ -27,6 +27,7 @@ PVS_DEVICE_TYPE, SETUP_TIMEOUT_MIN, SUNPOWER_COORDINATOR, + SUNPOWER_ENTRY_ID, SUNPOWER_HOST, SUNPOWER_OBJECT, SUNPOWER_UPDATE_INTERVAL, @@ -45,10 +46,8 @@ PLATFORMS = ["sensor", "binary_sensor"] -PREVIOUS_PVS_SAMPLE_TIME = 0 -PREVIOUS_PVS_SAMPLE = {} -PREVIOUS_ESS_SAMPLE_TIME = 0 -PREVIOUS_ESS_SAMPLE = {} +# Use entry-specific data storage to avoid conflicts between multiple accounts +ENTRY_DATA_CACHE = {} def create_vmeter(data): @@ -261,24 +260,30 @@ def sunpower_fetch( sunpower_monitor, sunpower_update_invertal, sunvault_update_invertal, + entry_id, ): """Basic data fetch routine to get and reformat sunpower data to a dict of device type and serial #""" - global PREVIOUS_PVS_SAMPLE_TIME - global PREVIOUS_PVS_SAMPLE - global PREVIOUS_ESS_SAMPLE_TIME - global PREVIOUS_ESS_SAMPLE - - sunpower_data = PREVIOUS_PVS_SAMPLE - ess_data = PREVIOUS_ESS_SAMPLE + # Use entry-specific cache to avoid conflicts between multiple accounts + if entry_id not in ENTRY_DATA_CACHE: + ENTRY_DATA_CACHE[entry_id] = { + "pvs_sample_time": 0, + "pvs_sample": {}, + "ess_sample_time": 0, + "ess_sample": {}, + } + + cache = ENTRY_DATA_CACHE[entry_id] + sunpower_data = cache["pvs_sample"] + ess_data = cache["ess_sample"] use_ess = False data = None try: - if (time.time() - PREVIOUS_PVS_SAMPLE_TIME) >= (sunpower_update_invertal - 1): - PREVIOUS_PVS_SAMPLE_TIME = time.time() + if (time.time() - cache["pvs_sample_time"]) >= (sunpower_update_invertal - 1): + cache["pvs_sample_time"] = time.time() sunpower_data = sunpower_monitor.device_list() - PREVIOUS_PVS_SAMPLE = sunpower_data + cache["pvs_sample"] = sunpower_data _LOGGER.debug("got PVS data %s", sunpower_data) except (ParseException, ConnectionException) as error: raise UpdateFailed from error @@ -288,10 +293,10 @@ def sunpower_fetch( use_ess = True try: - if use_ess and (time.time() - PREVIOUS_ESS_SAMPLE_TIME) >= (sunvault_update_invertal - 1): - PREVIOUS_ESS_SAMPLE_TIME = time.time() + if use_ess and (time.time() - cache["ess_sample_time"]) >= (sunvault_update_invertal - 1): + cache["ess_sample_time"] = time.time() ess_data = sunpower_monitor.energy_storage_system_status() - PREVIOUS_ESS_SAMPLE = ess_data + cache["ess_sample"] = ess_data _LOGGER.debug("got ESS data %s", ess_data) except (ParseException, ConnectionException) as error: raise UpdateFailed from error @@ -349,6 +354,7 @@ async def async_update_data(): sunpower_monitor, sunpower_update_invertal, sunvault_update_invertal, + entry_id, ) # This could be better, taking the shortest time interval as the coordinator update is fine @@ -375,6 +381,7 @@ async def async_update_data(): hass.data[DOMAIN][entry.entry_id] = { SUNPOWER_OBJECT: sunpower_monitor, SUNPOWER_COORDINATOR: coordinator, + SUNPOWER_ENTRY_ID: entry_id, } start = time.time() @@ -412,5 +419,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + # Clean up entry-specific cache to avoid memory leaks + if entry.entry_id in ENTRY_DATA_CACHE: + ENTRY_DATA_CACHE.pop(entry.entry_id) return unload_ok diff --git a/custom_components/sunpower/binary_sensor.py b/custom_components/sunpower/binary_sensor.py index 1f11b49..d2aba90 100644 --- a/custom_components/sunpower/binary_sensor.py +++ b/custom_components/sunpower/binary_sensor.py @@ -11,6 +11,7 @@ SUNPOWER_BINARY_SENSORS, SUNPOWER_COORDINATOR, SUNPOWER_DESCRIPTIVE_NAMES, + SUNPOWER_ENTRY_ID, SUNPOWER_PRODUCT_NAMES, SUNVAULT_BINARY_SENSORS, ) @@ -33,6 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): do_product_names = config_entry.data[SUNPOWER_PRODUCT_NAMES] coordinator = sunpower_state[SUNPOWER_COORDINATOR] + entry_id = sunpower_state[SUNPOWER_ENTRY_ID] sunpower_data = coordinator.data do_ess = False @@ -75,6 +77,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator=coordinator, my_info=sensor_data, parent_info=pvs if device_type != PVS_DEVICE_TYPE else None, + entry_id=entry_id, id_code=unique_id, device_type=device_type, field=sensor["field"], @@ -105,6 +108,7 @@ def __init__( coordinator, my_info, parent_info, + entry_id, id_code, device_type, field, @@ -113,7 +117,7 @@ def __init__( on_value, entity_category, ): - super().__init__(coordinator, my_info, parent_info) + super().__init__(coordinator, my_info, parent_info, entry_id) self._id_code = id_code self._device_type = device_type self._title = title @@ -141,12 +145,15 @@ def unique_id(self): """Device Uniqueid. https://developers.home-assistant.io/docs/entity_registry_index/#unique-id Should not include the domain, home assistant does that for us + Include entry_id to prevent conflicts between multiple accounts/locations base_unique_id is the serial number of the device (Inverter, PVS, Meter etc) "_pvs_" just as a divider - in case we start pulling data from some other source _field is the field within the data that this came from which is a dict so there is only one. Updating this format is a breaking change and should be called out if changed in a PR """ + if self._entry_id: + return f"{self._entry_id}_{self.base_unique_id}_pvs_{self._field}" return f"{self.base_unique_id}_pvs_{self._field}" @property diff --git a/custom_components/sunpower/config_flow.py b/custom_components/sunpower/config_flow.py index ea08c0b..963e59f 100644 --- a/custom_components/sunpower/config_flow.py +++ b/custom_components/sunpower/config_flow.py @@ -8,7 +8,10 @@ core, exceptions, ) -from homeassistant.const import CONF_HOST +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, +) from .const import ( DEFAULT_SUNPOWER_UPDATE_INTERVAL, @@ -32,6 +35,7 @@ DATA_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): str, + vol.Optional(CONF_NAME, default=""): str, vol.Required(SUNPOWER_DESCRIPTIVE_NAMES, default=True): bool, vol.Required(SUNPOWER_PRODUCT_NAMES, default=False): bool, }, @@ -45,7 +49,11 @@ async def validate_input(hass: core.HomeAssistant, data): """ spm = SunPowerMonitor(data[SUNPOWER_HOST]) - name = "PVS {}".format(data[SUNPOWER_HOST]) + # Use custom name if provided, otherwise use host IP + if data.get(CONF_NAME): + name = data[CONF_NAME] + else: + name = "PVS {}".format(data[SUNPOWER_HOST]) try: response = await hass.async_add_executor_job(spm.network_status) _LOGGER.debug("Got from %s %s", data[SUNPOWER_HOST], response) @@ -76,7 +84,15 @@ async def async_step_user(self, user_input: dict[str, any] | None = None): if user_input is not None: try: info = await validate_input(self.hass, user_input) - await self.async_set_unique_id(user_input[SUNPOWER_HOST]) + # Create unique_id combining host and name for better uniqueness + # across multiple accounts + unique_id = user_input[SUNPOWER_HOST] + if user_input.get(CONF_NAME): + host = user_input[SUNPOWER_HOST] + name = user_input[CONF_NAME] + unique_id = f"{host}_{name}" + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) except CannotConnect: errors["base"] = "cannot_connect" @@ -92,7 +108,14 @@ async def async_step_user(self, user_input: dict[str, any] | None = None): async def async_step_import(self, user_input: dict[str, any] | None = None): """Handle import.""" - await self.async_set_unique_id(user_input[SUNPOWER_HOST]) + # Create unique_id combining host and name for better uniqueness + # across multiple accounts + unique_id = user_input[SUNPOWER_HOST] + if user_input.get(CONF_NAME): + host = user_input[SUNPOWER_HOST] + name = user_input[CONF_NAME] + unique_id = f"{host}_{name}" + await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() return await self.async_step_user(user_input) diff --git a/custom_components/sunpower/const.py b/custom_components/sunpower/const.py index bbc59b9..88b265c 100644 --- a/custom_components/sunpower/const.py +++ b/custom_components/sunpower/const.py @@ -26,6 +26,7 @@ SUNPOWER_OBJECT = "sunpower" SUNPOWER_HOST = "host" SUNPOWER_COORDINATOR = "coordinator" +SUNPOWER_ENTRY_ID = "entry_id" DEFAULT_SUNPOWER_UPDATE_INTERVAL = 120 DEFAULT_SUNVAULT_UPDATE_INTERVAL = 60 MIN_SUNPOWER_UPDATE_INTERVAL = 60 diff --git a/custom_components/sunpower/entity.py b/custom_components/sunpower/entity.py index 67e25b4..31eb437 100644 --- a/custom_components/sunpower/entity.py +++ b/custom_components/sunpower/entity.py @@ -6,11 +6,12 @@ class SunPowerEntity(CoordinatorEntity): - def __init__(self, coordinator, my_info, parent_info): + def __init__(self, coordinator, my_info, parent_info, entry_id=None): """Initialize the sensor.""" super().__init__(coordinator) self._my_info = my_info self._parent_info = parent_info + self._entry_id = entry_id self.base_unique_id = self._my_info.get("SERIAL", "") @property @@ -21,16 +22,25 @@ def device_info(self): hw_version = self._my_info.get("HWVER", self._my_info.get("hw_version", "Unknown")) sw_version = self._my_info.get("SWVER", "Unknown") version = f"{sw_version} Hardware: {hw_version}" + # Include entry_id in device identifiers to prevent conflicts between multiple accounts + device_identifier = ( + f"{self._entry_id}_{self.base_unique_id}" if self._entry_id else self.base_unique_id + ) device_info = { - "identifiers": {(DOMAIN, self.base_unique_id)}, + "identifiers": {(DOMAIN, device_identifier)}, "name": name, "manufacturer": "SunPower", "model": model, "sw_version": version, } if self._parent_info is not None: + parent_identifier = ( + f"{self._entry_id}_{self._parent_info.get('SERIAL', 'UnknownParent')}" + if self._entry_id + else self._parent_info.get("SERIAL", "UnknownParent") + ) device_info["via_device"] = ( DOMAIN, - f"{self._parent_info.get('SERIAL', 'UnknownParent')}", + parent_identifier, ) return device_info diff --git a/custom_components/sunpower/sensor.py b/custom_components/sunpower/sensor.py index a7308ec..0b90e76 100644 --- a/custom_components/sunpower/sensor.py +++ b/custom_components/sunpower/sensor.py @@ -13,6 +13,7 @@ PVS_DEVICE_TYPE, SUNPOWER_COORDINATOR, SUNPOWER_DESCRIPTIVE_NAMES, + SUNPOWER_ENTRY_ID, SUNPOWER_PRODUCT_NAMES, SUNPOWER_SENSORS, SUNVAULT_SENSORS, @@ -36,6 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): do_product_names = config_entry.data[SUNPOWER_PRODUCT_NAMES] coordinator = sunpower_state[SUNPOWER_COORDINATOR] + entry_id = sunpower_state[SUNPOWER_ENTRY_ID] sunpower_data = coordinator.data do_ess = False @@ -78,6 +80,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator=coordinator, my_info=sensor_data, parent_info=pvs if device_type != PVS_DEVICE_TYPE else None, + entry_id=entry_id, id_code=unique_id, device_type=device_type, field=sensor["field"], @@ -109,6 +112,7 @@ def __init__( coordinator, my_info, parent_info, + entry_id, id_code, device_type, field, @@ -120,7 +124,7 @@ def __init__( entity_category, ): """Initialize the sensor.""" - super().__init__(coordinator, my_info, parent_info) + super().__init__(coordinator, my_info, parent_info, entry_id) self._id_code = id_code self._device_type = device_type self._title = title @@ -165,12 +169,15 @@ def unique_id(self): """Device Uniqueid. https://developers.home-assistant.io/docs/entity_registry_index/#unique-id Should not include the domain, home assistant does that for us + Include entry_id to prevent conflicts between multiple accounts/locations base_unique_id is the serial number of the device (Inverter, PVS, Meter etc) "_pvs_" just as a divider - in case we start pulling data from some other source _field is the field within the data that this came from which is a dict so there is only one. Updating this format is a breaking change and should be called out if changed in a PR """ + if self._entry_id: + return f"{self._entry_id}_{self.base_unique_id}_pvs_{self._field}" return f"{self.base_unique_id}_pvs_{self._field}" @property diff --git a/custom_components/sunpower/strings.json b/custom_components/sunpower/strings.json index c57b419..e9ff5d5 100644 --- a/custom_components/sunpower/strings.json +++ b/custom_components/sunpower/strings.json @@ -5,10 +5,11 @@ "user": { "data": { "host": "Host", + "name": "Location Name (optional)", "use_descriptive_names": "Use descriptive entity names (recommended)", "use_product_names": "Use products in entity names (not recommended)" }, - "description": "Hostname or IP of PVS (usually 172.27.153.1)" + "description": "Hostname or IP of PVS (usually 172.27.153.1). Use location name for multiple accounts." } }, "error": { diff --git a/custom_components/sunpower/translations/en.json b/custom_components/sunpower/translations/en.json index 0eb6620..3a7c51e 100644 --- a/custom_components/sunpower/translations/en.json +++ b/custom_components/sunpower/translations/en.json @@ -1,36 +1,37 @@ { - "config": { - "abort": { - "already_configured": "Already Configured" - }, - "error": { - "cannot_connect": "Cannot Connect", - "unknown": "Unknown Error" - }, - "step": { - "user": { + "config": { + "abort": { + "already_configured": "Already Configured" + }, + "error": { + "cannot_connect": "Cannot Connect", + "unknown": "Unknown Error" + }, + "step": { + "user": { "data": { - "host": "Host", - "use_descriptive_names": "Use descriptive entity names (recommended)", - "use_product_names": "Use products in entity names (not recommended)" + "host": "Host", + "name": "Location Name (optional)", + "use_descriptive_names": "Use descriptive entity names (recommended)", + "use_product_names": "Use products in entity names (not recommended)" }, - "description": "Hostname or IP of PVS (usually 172.27.153.1)" - } + "description": "Hostname or IP of PVS (usually 172.27.153.1). Use location name for multiple accounts." + } } - }, - "options":{ - "step": { - "init": { - "data": { - "PVS_UPDATE_INTERVAL": "Solar data update interval (not less than 60)", - "ESS_UPDATE_INTERVAL": "Energy storage update interval (not less than 20)" - }, - "description": "Update intervals to change the polling rate, note: the PVS is slow" - } + }, + "options": { + "step": { + "init": { + "data": { + "PVS_UPDATE_INTERVAL": "Solar data update interval (not less than 60)", + "ESS_UPDATE_INTERVAL": "Energy storage update interval (not less than 20)" }, - "error": { - "MIN_INTERVAL": "Interval too small" - } + "description": "Update intervals to change the polling rate, note: the PVS is slow" + } }, - "title": "SunPower" + "error": { + "MIN_INTERVAL": "Interval too small" + } + }, + "title": "SunPower" } diff --git a/env.example b/env.example new file mode 100644 index 0000000..7f502d2 --- /dev/null +++ b/env.example @@ -0,0 +1,18 @@ +# SunPower PVS Configuration +# Copy this file to .env and update with your settings + +# PVS Host IP Address +# This is typically 172.27.153.1 for NAT setup or your PVS's actual IP +PVS_HOST=172.27.153.1 + +# Optional: Custom name for your installation +PVS_NAME="My Solar System" + +# Debug logging level (DEBUG, INFO, WARNING, ERROR) +LOG_LEVEL=DEBUG + +# Home Assistant configuration (if testing with HA) +HA_CONFIG_DIR=/config + +# Test configuration +PYTEST_TIMEOUT=120 diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..83eb7f4 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,22 @@ + +# Development dependencies +black>=24.3.0 +flake8>=7.0.0 +flake8-bugbear>=24.2.6 +flake8-pytest>=1.4 +flake8-quotes>=3.4.0 +# Core dependencies +homeassistant>=2025.1.0 +isort>=5.13.2 + +# Type checking +mypy>=1.0.0 +pre-commit>=3.0.0 + +# Testing +pytest>=7.0.0 +pytest-cov +pytest-homeassistant-custom-component +requests +simplejson +types-requests diff --git a/testing/DEBUG_TOOLS_GUIDE.md b/testing/DEBUG_TOOLS_GUIDE.md new file mode 100644 index 0000000..71efcf6 --- /dev/null +++ b/testing/DEBUG_TOOLS_GUIDE.md @@ -0,0 +1,306 @@ +# SunPower Integration Debug Tools Guide + +This document explains all the debug tools available for developing and testing the SunPower Home Assistant integration. + +## ๐Ÿ› ๏ธ **Available Debug Tools** + +### **1. `test_api.py` - API Client Testing** + +**Purpose**: Test direct communication with PVS hardware +**Use When**: You want to verify PVS connectivity and API responses + +```bash +export PVS_HOST=192.168.1.100 # Your PVS IP +python test_api.py +``` + +**What it tests**: + +- โœ… Network connectivity to PVS +- โœ… Basic API commands (network status, device list) +- โœ… ESS functionality (if available) +- โœ… Error handling and timeouts + +**Example Output**: + +``` +Testing SunPower API connection to 192.168.1.100 +-------------------------------------------------- +1. Testing network status... +โœ… Network status successful +2. Testing device list... +โœ… Device list successful +Found 30 devices +``` + +--- + +### **2. `test_with_sample_data.py` - Integration Logic Testing** + +**Purpose**: Test integration logic without requiring real PVS hardware +**Use When**: You want to verify data processing, sensor mapping, and core logic + +```bash +python test_with_sample_data.py +``` + +**What it tests**: + +- โœ… Data conversion from PVS format to HA format +- โœ… Sensor field compatibility (which sensors will work) +- โœ… Virtual meter creation +- โœ… Device type processing +- โœ… Integration consistency + +**Example Output**: + +``` +๐Ÿงช Testing SunPower Integration with Sample Data +โœ… Data conversion successful +๐Ÿ“Š Device types found: ['PVS', 'Power Meter', 'Inverter'] +Sensor field compatibility: + - PVS: 9/9 fields available (100.0%) + - Power Meter: 8/16 fields available (50.0%) +``` + +--- + +### **3. `debug_runner.py` - Core Functionality Testing** + +**Purpose**: Test core integration functionality with real PVS data +**Use When**: You want to test the integration logic with real PVS responses + +```bash +export PVS_HOST=192.168.1.100 +python debug_runner.py +``` + +**What it tests**: + +- โœ… API client functionality +- โœ… Data conversion and processing +- โœ… Core integration functions +- โœ… Data fetch mechanisms +- โœ… Error handling and recovery + +**Example Output**: + +``` +๐Ÿ”ง Starting SunPower Integration Debug +1. Testing SunPower API client... +โœ… PVS connectivity successful +2. Testing data processing... +3. Testing data fetch function... +โœ… Data fetch function successful +``` + +--- + +### **4. `test_ha_integration.py` - Home Assistant Compatibility** + +**Purpose**: Test Home Assistant specific functionality (coordinators, etc.) +**Use When**: You want to verify HA integration components work correctly + +```bash +export PVS_HOST=192.168.1.100 +python test_ha_integration.py +``` + +**What it tests**: + +- โœ… DataUpdateCoordinator creation +- โœ… Home Assistant mock compatibility +- โœ… Integration data flow +- โœ… HA-specific error handling + +--- + +### **5. `pytest tests/` - Unit Testing** + +**Purpose**: Run automated unit tests +**Use When**: You want to verify code changes don't break existing functionality + +```bash +pytest tests/ -v +``` + +**What it tests**: + +- โœ… API client unit tests +- โœ… Mock response handling +- โœ… Error condition testing +- โœ… Edge case validation + +--- + +## ๐ŸŽฏ **Testing Workflow Recommendations** + +### **For New Development** + +1. **Start with**: `test_with_sample_data.py` (no hardware needed) +2. **Then test**: `debug_runner.py` (with real PVS) +3. **Finally verify**: `pytest tests/` (automated validation) + +### **For Bug Investigation** + +1. **Check connectivity**: `test_api.py` +2. **Verify logic**: `debug_runner.py` +3. **Test HA integration**: `test_ha_integration.py` + +### **For Performance Testing** + +1. **Use**: `debug_runner.py` with different update intervals +2. **Monitor**: PVS response times and error rates +3. **Validate**: Sample data processing speed + +--- + +## ๐Ÿ› **VS Code Debug Configurations** + +All scripts are available as VS Code debug configurations: + +| Configuration Name | Script | Purpose | +|-------------------|---------|---------| +| **Test with Sample Data** | `test_with_sample_data.py` | Test logic without hardware | +| **Debug SunPower Integration** | `debug_runner.py` | Test with real PVS | +| **Debug HA Integration** | `test_ha_integration.py` | Test HA compatibility | +| **Debug SunPower API Client** | `test_api.py` | Test API connectivity | + +**To use**: + +1. Press `F5` in VS Code +2. Select desired configuration +3. Set breakpoints anywhere in the code +4. Step through imports and execution + +--- + +## ๐Ÿ” **Debugging Import Issues** + +### **Step Through Import Resolution** + +1. Set breakpoint at top of any integration file +2. Use "Debug SunPower Integration" configuration +3. Step into (`F11`) import statements +4. Watch variables panel for `sys.path` and module loading + +### **Check Python Path** + +```python +# In debug console: +import sys +print('\n'.join(sys.path)) +``` + +### **Verify Module Loading** + +```python +# In debug console: +import custom_components.sunpower.const as const +print(dir(const)) +``` + +--- + +## ๐Ÿ“Š **Output Interpretation** + +### **Success Indicators** + +- โœ… Green checkmarks +- ๐Ÿ“Š Data statistics (device counts, field availability) +- ๐Ÿ“ˆ Performance metrics +- ๐ŸŽ‰ Completion messages + +### **Warning Indicators** + +- โš ๏ธ Yellow warnings (expected issues) +- โ„น๏ธ Informational messages +- ๐Ÿ’ก Helpful suggestions + +### **Error Indicators** + +- โŒ Red X marks +- ๐Ÿ” Troubleshooting sections +- Detailed error tracebacks + +--- + +## ๐Ÿ”ง **Troubleshooting Common Issues** + +### **"No module named 'sunpower'"** + +- Check Python path configuration +- Verify VS Code interpreter points to `./venv/bin/python` +- Restart VS Code language server + +### **Connection Timeouts** + +- Verify PVS IP address in `PVS_HOST` +- Check network connectivity: `curl http:///cgi-bin/dl_cgi?Command=Get_Comm` +- Try different timeout values + +### **Import Errors in Debug** + +- Use "Test with Sample Data" first (no external dependencies) +- Check that virtual environment is activated +- Verify all dependencies installed: `pip install -r requirements-dev.txt` + +--- + +## ๐Ÿš€ **Advanced Debugging Techniques** + +### **Custom Data Testing** + +1. Modify `samples/device_list.json` with your PVS data +2. Run `test_with_sample_data.py` to validate compatibility +3. Test edge cases with missing or malformed data + +### **Performance Profiling** + +```bash +python -m cProfile -o profile_stats debug_runner.py +``` + +### **Memory Usage Monitoring** + +```python +# Add to debug scripts: +import tracemalloc +tracemalloc.start() +# ... run tests ... +current, peak = tracemalloc.get_traced_memory() +print(f"Memory: {current / 1024 / 1024:.1f} MB") +``` + +--- + +## ๐Ÿ“š **Integration with Development Workflow** + +### **Before Committing Code** + +```bash +# 1. Run all tests +pytest tests/ -v + +# 2. Test with sample data +python test_with_sample_data.py + +# 3. Format code (automatic with pre-commit) +black custom_components/ tests/ *.py +``` + +### **Before Releasing** + +```bash +# 1. Test with real PVS +export PVS_HOST=your.pvs.ip +python debug_runner.py + +# 2. Test HA integration +python test_ha_integration.py + +# 3. Run full test suite +pytest tests/ --cov=custom_components/sunpower +``` + +This comprehensive debug tool suite ensures you can develop, test, and debug the SunPower integration efficiently at every stage of development. diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 0000000..44db95d --- /dev/null +++ b/testing/README.md @@ -0,0 +1,150 @@ +# SunPower Integration Testing Suite + +This directory contains comprehensive testing tools and documentation for the SunPower Home Assistant integration. + +## ๐Ÿ“ **Directory Structure** + +``` +testing/ +โ”œโ”€โ”€ samples/ +โ”‚ โ””โ”€โ”€ device_list.json # Sample PVS data for testing +โ”œโ”€โ”€ docs/ +โ”‚ โ”œโ”€โ”€ DEVELOPMENT_SETUP.md # Complete development setup guide +โ”‚ โ””โ”€โ”€ DEBUG_TOOLS_GUIDE.md # Detailed debug tools documentation +โ”œโ”€โ”€ test_api.py # Test PVS API connectivity +โ”œโ”€โ”€ test_with_sample_data.py # Test integration logic (no hardware) +โ”œโ”€โ”€ debug_runner.py # Test core functionality +โ”œโ”€โ”€ test_ha_integration.py # Test Home Assistant compatibility +โ””โ”€โ”€ README.md # This file +``` + +## ๐Ÿš€ **Quick Start** + +### **Test Without Hardware** (Recommended first step) + +```bash +cd testing +python test_with_sample_data.py +``` + +### **Test With Your PVS** + +```bash +cd testing +export PVS_HOST=192.168.1.100 # Your PVS IP +python test_api.py +python debug_runner.py +``` + +## ๐Ÿ› ๏ธ **Testing Tools** + +| Script | Purpose | Requires PVS | +|--------|---------|-------------| +| **`test_with_sample_data.py`** | Validate integration logic with sample data | โŒ | +| **`test_api.py`** | Test PVS connectivity and API responses | โœ… | +| **`debug_runner.py`** | Test core integration functionality | โœ… | +| **`test_ha_integration.py`** | Test Home Assistant compatibility | โœ… | + +## ๐Ÿ”ง **VS Code Integration** + +All test scripts are configured as VS Code debug targets: + +1. **Open** the main project directory in VS Code +2. **Press F5** to see debug configurations +3. **Select** your desired test script +4. **Set breakpoints** and debug through the code + +## ๐Ÿ“Š **Sample Data** + +The `samples/device_list.json` contains real PVS API response data for testing: + +- **1 PVS** (Photovoltaic Supervisor) +- **3 Power Meters** (including 1 virtual meter) +- **20 Inverters** (solar panel micro-inverters) + +This allows complete testing of the integration logic without needing physical hardware. + +## ๐Ÿ“š **Documentation** + +### **`docs/DEVELOPMENT_SETUP.md`** + +Complete guide for setting up the development environment including: + +- Python environment setup +- VS Code configuration +- Debugging setup +- Network configuration + +### **`docs/DEBUG_TOOLS_GUIDE.md`** + +Detailed documentation for all debug tools including: + +- Purpose and usage of each script +- Expected output examples +- Troubleshooting common issues +- Advanced debugging techniques + +## ๐ŸŽฏ **Development Workflow** + +### **For New Features** + +1. **Start**: `python test_with_sample_data.py` (validate logic) +2. **Test**: `python debug_runner.py` (test with real PVS) +3. **Verify**: `python test_ha_integration.py` (HA compatibility) + +### **For Bug Fixes** + +1. **Isolate**: `python test_api.py` (check connectivity) +2. **Debug**: Set breakpoints in VS Code and step through +3. **Validate**: Run all tests to ensure fix doesn't break anything + +### **Before Committing** + +```bash +# From project root +pytest tests/ -v # Unit tests +cd testing +python test_with_sample_data.py # Integration logic test +``` + +## ๐Ÿ†˜ **Troubleshooting** + +### **Import Errors** + +- Ensure you're running from the `testing/` directory +- Check that virtual environment is activated +- Verify Python path includes `custom_components` + +### **PVS Connection Issues** + +- Set `PVS_HOST` environment variable +- Test connectivity: `curl http://$PVS_HOST/cgi-bin/dl_cgi?Command=Get_Comm` +- Check network configuration (see development setup guide) + +### **VS Code Debug Issues** + +- Ensure Python interpreter points to `../venv/bin/python` +- Restart VS Code language server if needed +- Check that debug configurations point to correct file paths + +## ๐Ÿ“ˆ **Adding New Tests** + +When adding new test functionality: + +1. **Follow naming convention**: `test_*.py` +2. **Add VS Code debug config** in `../.vscode/launch.json` +3. **Update documentation** in `docs/DEBUG_TOOLS_GUIDE.md` +4. **Include error handling** and helpful output messages + +## ๐Ÿ”— **Related Files** + +- **Unit Tests**: `../tests/` (pytest-based unit tests) +- **VS Code Config**: `../.vscode/` (debug configurations, settings) +- **Integration Code**: `../custom_components/sunpower/` (main integration) +- **Requirements**: `../requirements-dev.txt` (development dependencies) + +--- + +For complete setup instructions, see [`docs/DEVELOPMENT_SETUP.md`](docs/DEVELOPMENT_SETUP.md). + +For detailed tool documentation, see [`docs/DEBUG_TOOLS_GUIDE.md`](docs/DEBUG_TOOLS_GUIDE.md). diff --git a/testing/debug_runner.py b/testing/debug_runner.py new file mode 100755 index 0000000..94e1c35 --- /dev/null +++ b/testing/debug_runner.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Debug runner for the SunPower integration. +This script simulates how Home Assistant would load and run the integration. +""" + +import asyncio +import logging +import os +import sys + +# Add the custom_components directory to Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "custom_components")) + +from sunpower import ( # noqa: E402 + convert_sunpower_data, + sunpower_fetch, +) +from sunpower.sunpower import SunPowerMonitor # noqa: E402 + +# Set up logging +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +async def debug_integration(): + """Debug the SunPower integration.""" + host = os.getenv("PVS_HOST", "172.27.153.1") + + print("๐Ÿ”ง Starting SunPower Integration Debug") + print(f"๐Ÿ“ก PVS Host: {host}") + print("-" * 50) + + try: + print("1. Testing SunPower API client...") + monitor = SunPowerMonitor(host) + + # Test basic connectivity + network_status = monitor.network_status() + print("โœ… PVS connectivity successful") + interface_count = len( + network_status.get("networkstatus", {}).get("interfaces", []), + ) + print(f"๐Ÿ“Š Network interfaces found: {interface_count}") + + # Test data fetching + device_data = monitor.device_list() + print(f"โœ… Retrieved {len(device_data.get('devices', []))} devices") + + # Test data conversion + converted_data = convert_sunpower_data(device_data) + print("โœ… Data conversion successful") + print(f"๐Ÿ“Š Device types found: {list(converted_data.keys())}") + + print("\n2. Testing data processing...") + + # Show device breakdown + for device_type, devices in converted_data.items(): + print(f" - {device_type}: {len(devices)} devices") + if devices: + sample_device = next(iter(devices.values())) + sample_fields = list(sample_device.keys())[:8] # First 8 fields + print(f" Sample fields: {sample_fields}") + + print("\n3. Testing data fetch function...") + + # Test the sunpower_fetch function directly + entry_id = "test_entry_123" + fetch_data = sunpower_fetch(monitor, 120, 60, entry_id) + + if fetch_data: + print("โœ… Data fetch function successful") + print(f"๐Ÿ“Š Fetched data keys: {list(fetch_data.keys())}") + + # Show data summary + total_devices = sum(len(devices) for devices in fetch_data.values()) + print(f"๐Ÿ“ˆ Total devices processed: {total_devices}") + + print("\n4. Testing ESS functionality...") + + try: + ess_data = monitor.energy_storage_system_status() + print("โœ… ESS data retrieval successful") + print(f"๐Ÿ“Š ESS result: {ess_data.get('result', 'Unknown')}") + except Exception as e: + print(f"โš ๏ธ ESS data failed (normal if no ESS): {e}") + + print("\n๐ŸŽ‰ All core functionality tests completed successfully!") + print("\n๐Ÿ’ก To test full Home Assistant integration:") + print(" 1. Install this integration in Home Assistant") + print(" 2. Use the configuration flow to set up your PVS") + print(" 3. Check entity registry for created sensors") + + except Exception as e: + print(f"โŒ Error during testing: {e}") + logger.exception("Full error details:") + + # Provide helpful troubleshooting info + print("\n๐Ÿ” Troubleshooting:") + print("- Check that PVS_HOST environment variable is set correctly") + print("- Verify PVS is accessible on your network") + print("- Ensure PVS management interface is connected") + print(f"- Try: curl 'http://{host}/cgi-bin/dl_cgi?Command=Get_Comm'") + + print("\nโœ… Debug session completed") + + +if __name__ == "__main__": + print("๐Ÿš€ SunPower Integration Debugger") + print("=" * 50) + print("This script helps debug the SunPower integration outside of Home Assistant.") + print("Set the PVS_HOST environment variable to your PVS IP address.") + print("Example: export PVS_HOST=192.168.1.100") + print("") + + # Run the debug session + asyncio.run(debug_integration()) diff --git a/samples/device_list.json b/testing/samples/device_list.json similarity index 100% rename from samples/device_list.json rename to testing/samples/device_list.json diff --git a/testing/test_api.py b/testing/test_api.py new file mode 100755 index 0000000..2ef3748 --- /dev/null +++ b/testing/test_api.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +Simple script to test the SunPower API client directly. +Set PVS_HOST environment variable or edit the host below. +""" + +import asyncio +import os +import sys +from pprint import pprint + +# Add the custom_components directory to Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "custom_components")) + +from sunpower.sunpower import ( # noqa: E402 + ConnectionException, + ParseException, + SunPowerMonitor, +) + + +async def test_api(): + """Test the SunPower API client.""" + # Change this to your PVS IP address + host = os.getenv("PVS_HOST", "172.27.153.1") # Default PVS IP for NAT setup + + print(f"Testing SunPower API connection to {host}") + print("-" * 50) + + monitor = SunPowerMonitor(host) + + try: + print("1. Testing network status...") + network_status = monitor.network_status() + print("โœ… Network status successful") + pprint(network_status) + print() + + except (ConnectionException, ParseException) as e: + print(f"โŒ Network status failed: {e}") + print("Check that:") + print("- PVS is accessible at the IP address") + print("- Network connectivity is working") + print("- PVS management interface is enabled") + return + + try: + print("2. Testing device list...") + device_list = monitor.device_list() + print("โœ… Device list successful") + print(f"Found {len(device_list.get('devices', []))} devices") + + # Show device summary + device_types = {} + for device in device_list.get("devices", []): + device_type = device.get("DEVICE_TYPE", "Unknown") + device_types[device_type] = device_types.get(device_type, 0) + 1 + + print("Device breakdown:") + for device_type, count in device_types.items(): + print(f" - {device_type}: {count}") + print() + + except (ConnectionException, ParseException) as e: + print(f"โŒ Device list failed: {e}") + print() + + try: + print("3. Testing energy storage system status...") + ess_status = monitor.energy_storage_system_status() + print("โœ… ESS status successful") + pprint(ess_status) + print() + + except (ConnectionException, ParseException) as e: + print(f"โŒ ESS status failed (this is normal if no ESS): {e}") + print() + + +if __name__ == "__main__": + # Run the async test + asyncio.run(test_api()) diff --git a/testing/test_ha_integration.py b/testing/test_ha_integration.py new file mode 100755 index 0000000..8895b7f --- /dev/null +++ b/testing/test_ha_integration.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +""" +Test Home Assistant integration with proper mocking. +This script tests the integration setup without requiring a full HA environment. +""" + +import asyncio +import logging +import os +import sys +from unittest.mock import ( + AsyncMock, + Mock, +) + +# Add the custom_components directory to Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "custom_components")) + +from homeassistant.config_entries import ConfigEntry # noqa: E402 +from homeassistant.core import HomeAssistant # noqa: E402 +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator # noqa: E402 +from sunpower.const import ( # noqa: E402 + DOMAIN, + SUNPOWER_DESCRIPTIVE_NAMES, + SUNPOWER_HOST, + SUNPOWER_PRODUCT_NAMES, +) + +# Set up logging +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +async def test_ha_integration(): + """Test the integration with a more realistic Home Assistant mock.""" + host = os.getenv("PVS_HOST", "172.27.153.1") + + print("๐Ÿ  Testing Home Assistant Integration") + print(f"๐Ÿ“ก PVS Host: {host}") + print("-" * 50) + + # Create a minimal mock HA that satisfies the coordinator requirements + hass = Mock(spec=HomeAssistant) + hass.data = {} + hass.loop = asyncio.get_event_loop() + hass.bus = Mock() + hass.bus.async_fire = AsyncMock() + hass.states = Mock() + + # Mock async_add_executor_job + async def mock_executor_job(func, *args): + return func(*args) + + hass.async_add_executor_job = mock_executor_job + + # Create config entry + config_entry = Mock(spec=ConfigEntry) + config_entry.entry_id = "test_entry_123" + config_entry.data = { + SUNPOWER_HOST: host, + SUNPOWER_DESCRIPTIVE_NAMES: True, + SUNPOWER_PRODUCT_NAMES: False, + } + config_entry.options = {} + + def mock_unload(func): + pass + + config_entry.async_on_unload = mock_unload + config_entry.add_update_listener = Mock(return_value=mock_unload) + + try: + print("1. Testing basic data structures...") + + # Initialize domain data + hass.data.setdefault(DOMAIN, {}) + print("โœ… Domain data initialized") + + print("\n2. Testing data coordinator creation...") + + # Create a simple coordinator without the full integration + from datetime import timedelta + + from sunpower import sunpower_fetch # noqa: E402 + from sunpower.sunpower import SunPowerMonitor # noqa: E402 + + monitor = SunPowerMonitor(host) + + async def async_update_data(): + """Test update function.""" + return sunpower_fetch(monitor, 120, 60, config_entry.entry_id) + + coordinator = DataUpdateCoordinator( + hass, + logger, + name="SunPower PVS Test", + update_method=async_update_data, + update_interval=timedelta(seconds=120), + ) + print("โœ… Data coordinator created successfully") + + print("\n3. Testing data fetch...") + + # Test data fetch + await coordinator.async_refresh() + + if coordinator.data: + print("โœ… Data fetch successful") + print(f"๐Ÿ“Š Data keys: {list(coordinator.data.keys())}") + + # Show device summary + total_devices = sum(len(devices) for devices in coordinator.data.values()) + print(f"๐Ÿ“ˆ Total devices: {total_devices}") + else: + print("โš ๏ธ No data returned from coordinator") + + print("\n๐ŸŽ‰ Home Assistant integration test completed successfully!") + + except Exception as e: + print(f"โŒ Error during HA integration testing: {e}") + logger.exception("Full error details:") + + # Provide helpful info + print("\n๐Ÿ” This test validates that:") + print("- DataUpdateCoordinator can be created") + print("- Data fetching works with the coordinator") + print("- Basic Home Assistant compatibility") + + print("\nโœ… HA integration test completed") + + +if __name__ == "__main__": + print("๐Ÿ  SunPower Home Assistant Integration Tester") + print("=" * 50) + print("This script tests the integration's compatibility with Home Assistant.") + print("Set PVS_HOST environment variable to test with a real PVS.") + print("") + + # Run the test + asyncio.run(test_ha_integration()) diff --git a/testing/test_with_sample_data.py b/testing/test_with_sample_data.py new file mode 100755 index 0000000..48724bd --- /dev/null +++ b/testing/test_with_sample_data.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +Test SunPower integration with sample data. +This script uses the sample device_list.json to test integration logic +without requiring a real PVS connection. +""" + +import json +import os +import sys + +# Add the custom_components directory to Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "custom_components")) + +from unittest.mock import Mock # noqa: E402 + +from sunpower import ( # noqa: E402 + convert_sunpower_data, + sunpower_fetch, +) +from sunpower.sunpower import SunPowerMonitor # noqa: E402 + + +def load_sample_data(): + """Load the sample device list data.""" + sample_file = os.path.join(os.path.dirname(__file__), "samples", "device_list.json") + + if not os.path.exists(sample_file): + print("โŒ Sample file not found: samples/device_list.json") + return None + + with open(sample_file, "r") as f: + return json.load(f) + + +def test_data_processing(): + """Test data processing with sample data.""" + print("๐Ÿงช Testing SunPower Integration with Sample Data") + print("=" * 50) + + # Load sample data + sample_data = load_sample_data() + if not sample_data: + return + + print(f"โœ… Loaded sample data with {len(sample_data.get('devices', []))} devices") + + print("\n1. Testing data conversion...") + + # Test data conversion + converted_data = convert_sunpower_data(sample_data) + print("โœ… Data conversion successful") + print(f"๐Ÿ“Š Device types found: {list(converted_data.keys())}") + + # Show detailed breakdown + print("\n2. Device breakdown:") + total_devices = 0 + for device_type, devices in converted_data.items(): + count = len(devices) + total_devices += count + print(f" - {device_type}: {count} devices") + + if devices: + # Show sample device info + sample_device = next(iter(devices.values())) + print(f" Serial example: {sample_device.get('SERIAL', 'Unknown')}") + print(f" Model example: {sample_device.get('MODEL', 'Unknown')}") + print(f" State example: {sample_device.get('STATE', 'Unknown')}") + + # Show available fields + field_count = len(sample_device.keys()) + sample_fields = list(sample_device.keys())[:6] # First 6 fields + print(f" Fields ({field_count} total): {sample_fields}") + print() + + print(f"๐Ÿ“ˆ Total devices processed: {total_devices}") + + print("\n3. Testing sensor mapping...") + + # Test sensor mapping by checking if key fields are available + from sunpower.const import ( # noqa: E402 + SUNPOWER_SENSORS, + SUNVAULT_SENSORS, + ) + + # Combine all sensors + all_sensors = {**SUNPOWER_SENSORS} + if any("ESS" in device_type for device_type in converted_data.keys()): + all_sensors.update(SUNVAULT_SENSORS) + print("โœ… ESS devices detected - SunVault sensors will be included") + else: + print("โ„น๏ธ No ESS devices - SunVault sensors not needed") + + # Check sensor field availability + sensor_compatibility = {} + for device_type, type_config in all_sensors.items(): + if device_type in converted_data: + devices = converted_data[device_type] + if devices: + sample_device = next(iter(devices.values())) + sensors = type_config["sensors"] + + available_fields = [] + missing_fields = [] + + for _sensor_name, sensor_config in sensors.items(): + field = sensor_config["field"] + if field in sample_device: + available_fields.append(field) + else: + missing_fields.append(field) + + sensor_compatibility[device_type] = { + "available": len(available_fields), + "missing": len(missing_fields), + "total": len(sensors), + "missing_fields": missing_fields[:3], # Show first 3 missing + } + + print("\n4. Sensor field compatibility:") + for device_type, stats in sensor_compatibility.items(): + available = stats["available"] + total = stats["total"] + percentage = (available / total * 100) if total > 0 else 0 + print(f" - {device_type}: {available}/{total} fields available ({percentage:.1f}%)") + + if stats["missing"] > 0: + missing_sample = stats["missing_fields"] + print(f" Missing examples: {missing_sample}") + + print("\n5. Testing virtual meter creation...") + + # Check if virtual meter was created + if "Power Meter" in converted_data: + meters = converted_data["Power Meter"] + virtual_meters = [m for serial, m in meters.items() if m.get("origin") == "virtual"] + + if virtual_meters: + print(f"โœ… Virtual meter created: {len(virtual_meters)} virtual meter(s)") + vm = virtual_meters[0] + print(f" - Serial: {vm.get('SERIAL')}") + print(f" - Type: {vm.get('TYPE')}") + print(f" - Model: {vm.get('MODEL')}") + print(f" - Power: {vm.get('p_3phsum_kw', 'N/A')} kW") + print(f" - Energy: {vm.get('net_ltea_3phsum_kwh', 'N/A')} kWh") + else: + print("โš ๏ธ No virtual meter found") + else: + print("โš ๏ธ No power meters found") + + print("\n6. Testing mock data fetch...") + + # Create a mock monitor that returns our sample data + mock_monitor = Mock(spec=SunPowerMonitor) + mock_monitor.device_list.return_value = sample_data + mock_monitor.energy_storage_system_status.return_value = {"result": "sample"} + + # Test the sunpower_fetch function + entry_id = "test_sample_entry" + try: + fetch_data = sunpower_fetch(mock_monitor, 120, 60, entry_id) + + if fetch_data: + print("โœ… Data fetch function successful") + print(f"๐Ÿ“Š Fetched data keys: {list(fetch_data.keys())}") + + # Compare with direct conversion + total_devices_fetch = sum(len(devices) for devices in fetch_data.values()) + print(f"๐Ÿ“ˆ Total devices from fetch: {total_devices_fetch}") + + if total_devices_fetch == total_devices: + print("โœ… Data consistency check passed") + else: + print("โš ๏ธ Data count mismatch between methods") + else: + print("โŒ Data fetch returned None") + + except Exception as e: + print(f"โŒ Data fetch failed: {e}") + + print("\n๐ŸŽ‰ Sample data testing completed!") + print("\n๐Ÿ’ก This validates that:") + print(" - Data conversion logic works correctly") + print(" - Sensor field mapping is compatible") + print(" - Virtual meter creation functions") + print(" - Core integration logic is sound") + + +if __name__ == "__main__": + test_data_processing() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..f019047 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests for SunPower Home Assistant Integration diff --git a/tests/test_sunpower_api.py b/tests/test_sunpower_api.py new file mode 100644 index 0000000..48ef789 --- /dev/null +++ b/tests/test_sunpower_api.py @@ -0,0 +1,74 @@ +"""Test the SunPower API client.""" + +import os +import sys +from unittest.mock import ( + Mock, + patch, +) + +import pytest +import requests + +# Add custom_components to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "custom_components")) + +from sunpower.sunpower import ( # noqa: E402 + ConnectionException, + SunPowerMonitor, +) + + +class TestSunPowerMonitor: + """Test the SunPowerMonitor class.""" + + def test_init(self): + """Test SunPowerMonitor initialization.""" + monitor = SunPowerMonitor("192.168.1.100") + assert monitor.host == "192.168.1.100" + assert monitor.command_url == "http://192.168.1.100/cgi-bin/dl_cgi?Command=" + + @patch("sunpower.sunpower.requests") + def test_device_list_success(self, mock_requests): + """Test successful device list retrieval.""" + # Mock response + mock_response = Mock() + mock_response.json.return_value = {"devices": []} + mock_requests.get.return_value = mock_response + + monitor = SunPowerMonitor("192.168.1.100") + result = monitor.device_list() + + assert result == {"devices": []} + mock_requests.get.assert_called_once_with( + "http://192.168.1.100/cgi-bin/dl_cgi?Command=DeviceList", + timeout=120, + ) + + @patch("sunpower.sunpower.requests") + def test_device_list_connection_error(self, mock_requests): + """Test device list with connection error.""" + # Mock the exception class as well + mock_requests.exceptions.RequestException = requests.exceptions.RequestException + mock_requests.get.side_effect = requests.exceptions.RequestException("Connection failed") + + monitor = SunPowerMonitor("192.168.1.100") + + with pytest.raises(ConnectionException): + monitor.device_list() + + @patch("sunpower.sunpower.requests") + def test_network_status_success(self, mock_requests): + """Test successful network status retrieval.""" + mock_response = Mock() + mock_response.json.return_value = {"status": "ok"} + mock_requests.get.return_value = mock_response + + monitor = SunPowerMonitor("192.168.1.100") + result = monitor.network_status() + + assert result == {"status": "ok"} + mock_requests.get.assert_called_once_with( + "http://192.168.1.100/cgi-bin/dl_cgi?Command=Get_Comm", + timeout=120, + ) From 0a10f436ce590de735380d47744f370019af2c5c Mon Sep 17 00:00:00 2001 From: Keith Baker Date: Tue, 9 Sep 2025 21:06:01 -0400 Subject: [PATCH 02/10] Revert Multi-PVS support for now (mainly to split commits) --- MULTIPLE_ACCOUNTS_UPDATE.md | 170 ------------------ custom_components/sunpower/__init__.py | 44 ++--- custom_components/sunpower/binary_sensor.py | 9 +- custom_components/sunpower/config_flow.py | 31 +--- custom_components/sunpower/const.py | 1 - custom_components/sunpower/entity.py | 16 +- custom_components/sunpower/sensor.py | 9 +- custom_components/sunpower/strings.json | 3 +- .../sunpower/translations/en.json | 59 +++--- 9 files changed, 56 insertions(+), 286 deletions(-) delete mode 100644 MULTIPLE_ACCOUNTS_UPDATE.md diff --git a/MULTIPLE_ACCOUNTS_UPDATE.md b/MULTIPLE_ACCOUNTS_UPDATE.md deleted file mode 100644 index 1146a0a..0000000 --- a/MULTIPLE_ACCOUNTS_UPDATE.md +++ /dev/null @@ -1,170 +0,0 @@ -# Multiple Accounts/Locations Support Update - -## Overview - -This update enables the SunPower Home Assistant integration to support multiple accounts and locations without sensor or device conflicts. Each account/location installation is now properly isolated with unique identifiers. - -## Changes Made - -### 1. Config Flow Updates (`config_flow.py`) - -- Added optional `name` field for location identification -- Updated unique_id generation to include location name when provided -- Enhanced validation and error handling -- Updated UI strings and translations - -### 2. Core Architecture Changes (`__init__.py`) - -- **Fixed global data storage issue**: Replaced global variables with entry-specific cache (`ENTRY_DATA_CACHE`) -- Updated `sunpower_fetch()` to accept and use `entry_id` parameter -- Added proper cleanup in `async_unload_entry()` to prevent memory leaks -- Enhanced data isolation between multiple installations - -### 3. Entity Identification System (`entity.py`, `sensor.py`, `binary_sensor.py`) - -- **Enhanced unique_id generation**: All entities now include entry_id in their unique_id to prevent conflicts -- **Updated device identifiers**: Device identifiers now include entry_id to ensure device uniqueness -- **Backwards compatibility**: System falls back to old format when entry_id is not available -- Added entry_id parameter to all entity constructors - -### 4. Translation Updates - -- Updated `strings.json` and `translations/en.json` with new location name field -- Enhanced descriptions to guide users on multiple account setup - -## Key Features - -### Conflict Prevention - -- **Entity unique_id format**: `{entry_id}_{device_serial}_pvs_{field}` (vs old: `{device_serial}_pvs_{field}`) -- **Device identifiers**: `(domain, "{entry_id}_{device_serial}")` (vs old: `(domain, "{device_serial}")`) -- **Config entry unique_id**: `{host}_{location_name}` (vs old: `{host}`) - -### Data Isolation - -- Each account/location has its own data cache -- No shared global variables between installations -- Proper cleanup on entry removal - -### User Experience - -- Optional location name field for easy identification -- Clear UI guidance for multiple account setup -- Backwards compatible with existing installations - -## Testing Plan - -### 1. Single Account (Backwards Compatibility) - -1. Install integration without location name -2. Verify all sensors and devices work as before -3. Check entity unique_ids maintain backwards compatibility - -### 2. Multiple Accounts Setup - -1. **First Account**: - - Configure with Host: `192.168.1.100` and Name: `Home Main` - - Verify all entities are created with format: `{entry_id}_...` - -2. **Second Account**: - - Configure with Host: `192.168.1.101` and Name: `Cabin Solar` - - Verify no conflicts with first account - - Check that devices and entities are properly separated - -3. **Same Host, Different Names**: - - Configure with Host: `192.168.1.100` and Name: `Home Backup` - - Verify this creates a separate integration instance - - Confirm no entity or device conflicts - -### 3. Conflict Validation - -1. Check Home Assistant's Developer Tools โ†’ States -2. Verify no duplicate entity IDs exist -3. Confirm device registry shows separate devices for each account -4. Test that data updates work independently for each account - -### 4. Cleanup Testing - -1. Remove one integration instance -2. Verify the cache is properly cleaned up -3. Confirm other instances continue working normally -4. Check for memory leaks in logs - -## Migration Notes - -### For Existing Users - -- **No action required**: Existing installations will continue to work unchanged -- Entity unique_ids remain the same for backwards compatibility -- Devices maintain their current identifiers - -### For New Multi-Account Users - -1. When adding a second account, **always use the location name field** -2. Use descriptive names like "Main House", "Cabin", "Garage", etc. -3. Each location should have a unique combination of host and name - -## Technical Details - -### Entry-Specific Data Cache - -```python -ENTRY_DATA_CACHE = { - "entry_id_1": { - "pvs_sample_time": 0, - "pvs_sample": {}, - "ess_sample_time": 0, - "ess_sample": {} - }, - "entry_id_2": { - # ... separate cache for another account - } -} -``` - -### Entity Unique ID Examples - -```python -# Old format (still used for backwards compatibility) -"E00202040011392_pvs_ltea_3phsum_kwh" - -# New format with entry_id -"abc123def456_E00202040011392_pvs_ltea_3phsum_kwh" -``` - -### Device Identifier Examples - -```python -# Old format -("sunpower", "E00202040011392") - -# New format -("sunpower", "abc123def456_E00202040011392") -``` - -## Troubleshooting - -### If You See Entity Conflicts - -1. Ensure each account uses a unique location name -2. Restart Home Assistant after configuration changes -3. Check Developer Tools โ†’ States for duplicate entities - -### Performance Considerations - -- Each account maintains its own update intervals -- Data caching is isolated per account to prevent interference -- Memory usage scales linearly with number of accounts - -## Future Enhancements - -Potential future improvements: - -1. Bulk account configuration -2. Account grouping in UI -3. Cross-account energy summary -4. Advanced conflict detection and resolution - -## Breaking Changes - -**None** - This update is fully backwards compatible with existing installations. diff --git a/custom_components/sunpower/__init__.py b/custom_components/sunpower/__init__.py index 1e51d35..ba16edc 100644 --- a/custom_components/sunpower/__init__.py +++ b/custom_components/sunpower/__init__.py @@ -27,7 +27,6 @@ PVS_DEVICE_TYPE, SETUP_TIMEOUT_MIN, SUNPOWER_COORDINATOR, - SUNPOWER_ENTRY_ID, SUNPOWER_HOST, SUNPOWER_OBJECT, SUNPOWER_UPDATE_INTERVAL, @@ -46,8 +45,10 @@ PLATFORMS = ["sensor", "binary_sensor"] -# Use entry-specific data storage to avoid conflicts between multiple accounts -ENTRY_DATA_CACHE = {} +PREVIOUS_PVS_SAMPLE_TIME = 0 +PREVIOUS_PVS_SAMPLE = {} +PREVIOUS_ESS_SAMPLE_TIME = 0 +PREVIOUS_ESS_SAMPLE = {} def create_vmeter(data): @@ -260,30 +261,24 @@ def sunpower_fetch( sunpower_monitor, sunpower_update_invertal, sunvault_update_invertal, - entry_id, ): """Basic data fetch routine to get and reformat sunpower data to a dict of device type and serial #""" - # Use entry-specific cache to avoid conflicts between multiple accounts - if entry_id not in ENTRY_DATA_CACHE: - ENTRY_DATA_CACHE[entry_id] = { - "pvs_sample_time": 0, - "pvs_sample": {}, - "ess_sample_time": 0, - "ess_sample": {}, - } - - cache = ENTRY_DATA_CACHE[entry_id] - sunpower_data = cache["pvs_sample"] - ess_data = cache["ess_sample"] + global PREVIOUS_PVS_SAMPLE_TIME + global PREVIOUS_PVS_SAMPLE + global PREVIOUS_ESS_SAMPLE_TIME + global PREVIOUS_ESS_SAMPLE + + sunpower_data = PREVIOUS_PVS_SAMPLE + ess_data = PREVIOUS_ESS_SAMPLE use_ess = False data = None try: - if (time.time() - cache["pvs_sample_time"]) >= (sunpower_update_invertal - 1): - cache["pvs_sample_time"] = time.time() + if (time.time() - PREVIOUS_PVS_SAMPLE_TIME) >= (sunpower_update_invertal - 1): + PREVIOUS_PVS_SAMPLE_TIME = time.time() sunpower_data = sunpower_monitor.device_list() - cache["pvs_sample"] = sunpower_data + PREVIOUS_PVS_SAMPLE = sunpower_data _LOGGER.debug("got PVS data %s", sunpower_data) except (ParseException, ConnectionException) as error: raise UpdateFailed from error @@ -293,10 +288,10 @@ def sunpower_fetch( use_ess = True try: - if use_ess and (time.time() - cache["ess_sample_time"]) >= (sunvault_update_invertal - 1): - cache["ess_sample_time"] = time.time() + if use_ess and (time.time() - PREVIOUS_ESS_SAMPLE_TIME) >= (sunvault_update_invertal - 1): + PREVIOUS_ESS_SAMPLE_TIME = time.time() ess_data = sunpower_monitor.energy_storage_system_status() - cache["ess_sample"] = ess_data + PREVIOUS_ESS_SAMPLE = ess_data _LOGGER.debug("got ESS data %s", ess_data) except (ParseException, ConnectionException) as error: raise UpdateFailed from error @@ -354,7 +349,6 @@ async def async_update_data(): sunpower_monitor, sunpower_update_invertal, sunvault_update_invertal, - entry_id, ) # This could be better, taking the shortest time interval as the coordinator update is fine @@ -381,7 +375,6 @@ async def async_update_data(): hass.data[DOMAIN][entry.entry_id] = { SUNPOWER_OBJECT: sunpower_monitor, SUNPOWER_COORDINATOR: coordinator, - SUNPOWER_ENTRY_ID: entry_id, } start = time.time() @@ -419,8 +412,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - # Clean up entry-specific cache to avoid memory leaks - if entry.entry_id in ENTRY_DATA_CACHE: - ENTRY_DATA_CACHE.pop(entry.entry_id) return unload_ok diff --git a/custom_components/sunpower/binary_sensor.py b/custom_components/sunpower/binary_sensor.py index d2aba90..1f11b49 100644 --- a/custom_components/sunpower/binary_sensor.py +++ b/custom_components/sunpower/binary_sensor.py @@ -11,7 +11,6 @@ SUNPOWER_BINARY_SENSORS, SUNPOWER_COORDINATOR, SUNPOWER_DESCRIPTIVE_NAMES, - SUNPOWER_ENTRY_ID, SUNPOWER_PRODUCT_NAMES, SUNVAULT_BINARY_SENSORS, ) @@ -34,7 +33,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): do_product_names = config_entry.data[SUNPOWER_PRODUCT_NAMES] coordinator = sunpower_state[SUNPOWER_COORDINATOR] - entry_id = sunpower_state[SUNPOWER_ENTRY_ID] sunpower_data = coordinator.data do_ess = False @@ -77,7 +75,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator=coordinator, my_info=sensor_data, parent_info=pvs if device_type != PVS_DEVICE_TYPE else None, - entry_id=entry_id, id_code=unique_id, device_type=device_type, field=sensor["field"], @@ -108,7 +105,6 @@ def __init__( coordinator, my_info, parent_info, - entry_id, id_code, device_type, field, @@ -117,7 +113,7 @@ def __init__( on_value, entity_category, ): - super().__init__(coordinator, my_info, parent_info, entry_id) + super().__init__(coordinator, my_info, parent_info) self._id_code = id_code self._device_type = device_type self._title = title @@ -145,15 +141,12 @@ def unique_id(self): """Device Uniqueid. https://developers.home-assistant.io/docs/entity_registry_index/#unique-id Should not include the domain, home assistant does that for us - Include entry_id to prevent conflicts between multiple accounts/locations base_unique_id is the serial number of the device (Inverter, PVS, Meter etc) "_pvs_" just as a divider - in case we start pulling data from some other source _field is the field within the data that this came from which is a dict so there is only one. Updating this format is a breaking change and should be called out if changed in a PR """ - if self._entry_id: - return f"{self._entry_id}_{self.base_unique_id}_pvs_{self._field}" return f"{self.base_unique_id}_pvs_{self._field}" @property diff --git a/custom_components/sunpower/config_flow.py b/custom_components/sunpower/config_flow.py index 963e59f..ea08c0b 100644 --- a/custom_components/sunpower/config_flow.py +++ b/custom_components/sunpower/config_flow.py @@ -8,10 +8,7 @@ core, exceptions, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, -) +from homeassistant.const import CONF_HOST from .const import ( DEFAULT_SUNPOWER_UPDATE_INTERVAL, @@ -35,7 +32,6 @@ DATA_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): str, - vol.Optional(CONF_NAME, default=""): str, vol.Required(SUNPOWER_DESCRIPTIVE_NAMES, default=True): bool, vol.Required(SUNPOWER_PRODUCT_NAMES, default=False): bool, }, @@ -49,11 +45,7 @@ async def validate_input(hass: core.HomeAssistant, data): """ spm = SunPowerMonitor(data[SUNPOWER_HOST]) - # Use custom name if provided, otherwise use host IP - if data.get(CONF_NAME): - name = data[CONF_NAME] - else: - name = "PVS {}".format(data[SUNPOWER_HOST]) + name = "PVS {}".format(data[SUNPOWER_HOST]) try: response = await hass.async_add_executor_job(spm.network_status) _LOGGER.debug("Got from %s %s", data[SUNPOWER_HOST], response) @@ -84,15 +76,7 @@ async def async_step_user(self, user_input: dict[str, any] | None = None): if user_input is not None: try: info = await validate_input(self.hass, user_input) - # Create unique_id combining host and name for better uniqueness - # across multiple accounts - unique_id = user_input[SUNPOWER_HOST] - if user_input.get(CONF_NAME): - host = user_input[SUNPOWER_HOST] - name = user_input[CONF_NAME] - unique_id = f"{host}_{name}" - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() + await self.async_set_unique_id(user_input[SUNPOWER_HOST]) return self.async_create_entry(title=info["title"], data=user_input) except CannotConnect: errors["base"] = "cannot_connect" @@ -108,14 +92,7 @@ async def async_step_user(self, user_input: dict[str, any] | None = None): async def async_step_import(self, user_input: dict[str, any] | None = None): """Handle import.""" - # Create unique_id combining host and name for better uniqueness - # across multiple accounts - unique_id = user_input[SUNPOWER_HOST] - if user_input.get(CONF_NAME): - host = user_input[SUNPOWER_HOST] - name = user_input[CONF_NAME] - unique_id = f"{host}_{name}" - await self.async_set_unique_id(unique_id) + await self.async_set_unique_id(user_input[SUNPOWER_HOST]) self._abort_if_unique_id_configured() return await self.async_step_user(user_input) diff --git a/custom_components/sunpower/const.py b/custom_components/sunpower/const.py index 88b265c..bbc59b9 100644 --- a/custom_components/sunpower/const.py +++ b/custom_components/sunpower/const.py @@ -26,7 +26,6 @@ SUNPOWER_OBJECT = "sunpower" SUNPOWER_HOST = "host" SUNPOWER_COORDINATOR = "coordinator" -SUNPOWER_ENTRY_ID = "entry_id" DEFAULT_SUNPOWER_UPDATE_INTERVAL = 120 DEFAULT_SUNVAULT_UPDATE_INTERVAL = 60 MIN_SUNPOWER_UPDATE_INTERVAL = 60 diff --git a/custom_components/sunpower/entity.py b/custom_components/sunpower/entity.py index 31eb437..67e25b4 100644 --- a/custom_components/sunpower/entity.py +++ b/custom_components/sunpower/entity.py @@ -6,12 +6,11 @@ class SunPowerEntity(CoordinatorEntity): - def __init__(self, coordinator, my_info, parent_info, entry_id=None): + def __init__(self, coordinator, my_info, parent_info): """Initialize the sensor.""" super().__init__(coordinator) self._my_info = my_info self._parent_info = parent_info - self._entry_id = entry_id self.base_unique_id = self._my_info.get("SERIAL", "") @property @@ -22,25 +21,16 @@ def device_info(self): hw_version = self._my_info.get("HWVER", self._my_info.get("hw_version", "Unknown")) sw_version = self._my_info.get("SWVER", "Unknown") version = f"{sw_version} Hardware: {hw_version}" - # Include entry_id in device identifiers to prevent conflicts between multiple accounts - device_identifier = ( - f"{self._entry_id}_{self.base_unique_id}" if self._entry_id else self.base_unique_id - ) device_info = { - "identifiers": {(DOMAIN, device_identifier)}, + "identifiers": {(DOMAIN, self.base_unique_id)}, "name": name, "manufacturer": "SunPower", "model": model, "sw_version": version, } if self._parent_info is not None: - parent_identifier = ( - f"{self._entry_id}_{self._parent_info.get('SERIAL', 'UnknownParent')}" - if self._entry_id - else self._parent_info.get("SERIAL", "UnknownParent") - ) device_info["via_device"] = ( DOMAIN, - parent_identifier, + f"{self._parent_info.get('SERIAL', 'UnknownParent')}", ) return device_info diff --git a/custom_components/sunpower/sensor.py b/custom_components/sunpower/sensor.py index 0b90e76..a7308ec 100644 --- a/custom_components/sunpower/sensor.py +++ b/custom_components/sunpower/sensor.py @@ -13,7 +13,6 @@ PVS_DEVICE_TYPE, SUNPOWER_COORDINATOR, SUNPOWER_DESCRIPTIVE_NAMES, - SUNPOWER_ENTRY_ID, SUNPOWER_PRODUCT_NAMES, SUNPOWER_SENSORS, SUNVAULT_SENSORS, @@ -37,7 +36,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): do_product_names = config_entry.data[SUNPOWER_PRODUCT_NAMES] coordinator = sunpower_state[SUNPOWER_COORDINATOR] - entry_id = sunpower_state[SUNPOWER_ENTRY_ID] sunpower_data = coordinator.data do_ess = False @@ -80,7 +78,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator=coordinator, my_info=sensor_data, parent_info=pvs if device_type != PVS_DEVICE_TYPE else None, - entry_id=entry_id, id_code=unique_id, device_type=device_type, field=sensor["field"], @@ -112,7 +109,6 @@ def __init__( coordinator, my_info, parent_info, - entry_id, id_code, device_type, field, @@ -124,7 +120,7 @@ def __init__( entity_category, ): """Initialize the sensor.""" - super().__init__(coordinator, my_info, parent_info, entry_id) + super().__init__(coordinator, my_info, parent_info) self._id_code = id_code self._device_type = device_type self._title = title @@ -169,15 +165,12 @@ def unique_id(self): """Device Uniqueid. https://developers.home-assistant.io/docs/entity_registry_index/#unique-id Should not include the domain, home assistant does that for us - Include entry_id to prevent conflicts between multiple accounts/locations base_unique_id is the serial number of the device (Inverter, PVS, Meter etc) "_pvs_" just as a divider - in case we start pulling data from some other source _field is the field within the data that this came from which is a dict so there is only one. Updating this format is a breaking change and should be called out if changed in a PR """ - if self._entry_id: - return f"{self._entry_id}_{self.base_unique_id}_pvs_{self._field}" return f"{self.base_unique_id}_pvs_{self._field}" @property diff --git a/custom_components/sunpower/strings.json b/custom_components/sunpower/strings.json index e9ff5d5..c57b419 100644 --- a/custom_components/sunpower/strings.json +++ b/custom_components/sunpower/strings.json @@ -5,11 +5,10 @@ "user": { "data": { "host": "Host", - "name": "Location Name (optional)", "use_descriptive_names": "Use descriptive entity names (recommended)", "use_product_names": "Use products in entity names (not recommended)" }, - "description": "Hostname or IP of PVS (usually 172.27.153.1). Use location name for multiple accounts." + "description": "Hostname or IP of PVS (usually 172.27.153.1)" } }, "error": { diff --git a/custom_components/sunpower/translations/en.json b/custom_components/sunpower/translations/en.json index 3a7c51e..0eb6620 100644 --- a/custom_components/sunpower/translations/en.json +++ b/custom_components/sunpower/translations/en.json @@ -1,37 +1,36 @@ { - "config": { - "abort": { - "already_configured": "Already Configured" - }, - "error": { - "cannot_connect": "Cannot Connect", - "unknown": "Unknown Error" - }, - "step": { - "user": { + "config": { + "abort": { + "already_configured": "Already Configured" + }, + "error": { + "cannot_connect": "Cannot Connect", + "unknown": "Unknown Error" + }, + "step": { + "user": { "data": { - "host": "Host", - "name": "Location Name (optional)", - "use_descriptive_names": "Use descriptive entity names (recommended)", - "use_product_names": "Use products in entity names (not recommended)" + "host": "Host", + "use_descriptive_names": "Use descriptive entity names (recommended)", + "use_product_names": "Use products in entity names (not recommended)" }, - "description": "Hostname or IP of PVS (usually 172.27.153.1). Use location name for multiple accounts." - } + "description": "Hostname or IP of PVS (usually 172.27.153.1)" + } } - }, - "options": { - "step": { - "init": { - "data": { - "PVS_UPDATE_INTERVAL": "Solar data update interval (not less than 60)", - "ESS_UPDATE_INTERVAL": "Energy storage update interval (not less than 20)" + }, + "options":{ + "step": { + "init": { + "data": { + "PVS_UPDATE_INTERVAL": "Solar data update interval (not less than 60)", + "ESS_UPDATE_INTERVAL": "Energy storage update interval (not less than 20)" + }, + "description": "Update intervals to change the polling rate, note: the PVS is slow" + } }, - "description": "Update intervals to change the polling rate, note: the PVS is slow" - } + "error": { + "MIN_INTERVAL": "Interval too small" + } }, - "error": { - "MIN_INTERVAL": "Interval too small" - } - }, - "title": "SunPower" + "title": "SunPower" } From ff885c48ed007aca197ffe1174fe0a2fd2801645 Mon Sep 17 00:00:00 2001 From: Keith Baker Date: Tue, 9 Sep 2025 21:15:19 -0400 Subject: [PATCH 03/10] Updating to pass pre-commits --- DEVELOPMENT_SETUP.md | 7 +++++-- testing/DEBUG_TOOLS_GUIDE.md | 12 +++++++----- testing/README.md | 6 ++++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/DEVELOPMENT_SETUP.md b/DEVELOPMENT_SETUP.md index de4b456..79e1af5 100644 --- a/DEVELOPMENT_SETUP.md +++ b/DEVELOPMENT_SETUP.md @@ -1,10 +1,13 @@ # SunPower Integration Development Setup -This guide will help you set up a complete development environment for the SunPower Home Assistant integration, including debugging capabilities and import stepping. +This guide will help you set up a complete development environment +for the SunPower Home Assistant integration, including debugging capabilities and import stepping. ## ๐Ÿ—๏ธ **Project Overview** -This is a **Home Assistant custom integration** for monitoring SunPower solar systems via local PVS (Photovoltaic Supervisor) interface. The integration provides real-time data for: +This is a **Home Assistant custom integration** for monitoring SunPower +solar systems via local PVS (Photovoltaic Supervisor) interface. The +integration provides real-time data for: - Solar panel production (per-panel data) - Power consumption and grid interaction diff --git a/testing/DEBUG_TOOLS_GUIDE.md b/testing/DEBUG_TOOLS_GUIDE.md index 71efcf6..8a99ad5 100644 --- a/testing/DEBUG_TOOLS_GUIDE.md +++ b/testing/DEBUG_TOOLS_GUIDE.md @@ -1,6 +1,7 @@ # SunPower Integration Debug Tools Guide -This document explains all the debug tools available for developing and testing the SunPower Home Assistant integration. +This document explains all the debug tools available for +developing and testing the SunPower Home Assistant integration. ## ๐Ÿ› ๏ธ **Available Debug Tools** @@ -23,7 +24,7 @@ python test_api.py **Example Output**: -``` +```console Testing SunPower API connection to 192.168.1.100 -------------------------------------------------- 1. Testing network status... @@ -54,7 +55,7 @@ python test_with_sample_data.py **Example Output**: -``` +```console ๐Ÿงช Testing SunPower Integration with Sample Data โœ… Data conversion successful ๐Ÿ“Š Device types found: ['PVS', 'Power Meter', 'Inverter'] @@ -85,7 +86,7 @@ python debug_runner.py **Example Output**: -``` +```console ๐Ÿ”ง Starting SunPower Integration Debug 1. Testing SunPower API client... โœ… PVS connectivity successful @@ -303,4 +304,5 @@ python test_ha_integration.py pytest tests/ --cov=custom_components/sunpower ``` -This comprehensive debug tool suite ensures you can develop, test, and debug the SunPower integration efficiently at every stage of development. +This comprehensive debug tool suite ensures you can develop, test, and +debug the SunPower integration efficiently at every stage of development. diff --git a/testing/README.md b/testing/README.md index 44db95d..c713331 100644 --- a/testing/README.md +++ b/testing/README.md @@ -1,10 +1,12 @@ # SunPower Integration Testing Suite -This directory contains comprehensive testing tools and documentation for the SunPower Home Assistant integration. +This directory contains comprehensive testing tools +and documentation for the SunPower Home Assistant +integration. ## ๐Ÿ“ **Directory Structure** -``` +```console testing/ โ”œโ”€โ”€ samples/ โ”‚ โ””โ”€โ”€ device_list.json # Sample PVS data for testing From ead27c467baf6d29821d3b8686a59de9b342629b Mon Sep 17 00:00:00 2001 From: Keith Baker Date: Wed, 10 Sep 2025 02:49:10 +0000 Subject: [PATCH 04/10] Do a bit more automation of vscode, update docs --- .devcontainer/devcontainer.json | 30 +++++++++++++++++++++++++++++ .pre-commit-config.yaml | 1 + .vscode/settings.json | 3 +++ DEVELOPMENT_SETUP.md | 34 +++++++++++++++++---------------- 4 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..198404b --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,30 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "Python 3", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bookworm", + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.black-formatter", + "ms-python.isort", + "ms-python.flake8", + "eamodio.gitlens" + ] + } + }, + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + "postCreateCommand": "virtualenv venv && ./venv/bin/pip install -r requirements-dev.txt && ./venv/bin/pre-commit install" + // Configure tool-specific properties. + // "customizations": {}, + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c8e920d..079900f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,6 +34,7 @@ repos: - id: check-yaml - id: check-json + exclude: ^.devcontainer/devcontainer.json$ - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.39.0 diff --git a/.vscode/settings.json b/.vscode/settings.json index 5bf4f2f..ed72745 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,7 +13,10 @@ "--profile", "black" ], + "editor.insertSpaces": true, + "editor.tabSize": 2, "editor.formatOnSave": true, + "editor.detectIndentation": false, "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, diff --git a/DEVELOPMENT_SETUP.md b/DEVELOPMENT_SETUP.md index 79e1af5..61a802c 100644 --- a/DEVELOPMENT_SETUP.md +++ b/DEVELOPMENT_SETUP.md @@ -16,7 +16,7 @@ integration provides real-time data for: ## ๐Ÿ“‹ **Prerequisites** -- **Python 3.11+** (tested with 3.13.5) +- **Python 3.12+** (tested with 3.12.11, 3.13.5) - **VS Code** (recommended) or PyCharm - **Git** for version control - Access to a **SunPower PVS system** (for testing) @@ -31,43 +31,45 @@ git clone https://github.com/krbaker/hass-sunpower.git cd hass-sunpower ``` -### 2. **Create Virtual Environment** +### 3. **Configure Environment** + +```bash +cp env.example .env +# Edit .env with your PVS IP address +``` + +## ๐Ÿ”ง **VS Code Setup** + +The project includes pre-configured VS Code settings for optimal development. +The devcontainer should setup all of the following: + +### **Python Virtual Environment (DEVCONTAINER DOES THIS FOR YOU)** ```bash python3 -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate ``` -### 3. **Install Dependencies** +### 3. **Install Dependencies (DEVCONTAINER DOES THIS FOR YOU)** ```bash pip install --upgrade pip pip install -r requirements-dev.txt ``` -### 4. **Setup Pre-commit Hooks** +### 2. **Setup Pre-commit Hooks (DEVCONTAINER DOES THIS FOR YOU)** ```bash pre-commit install ``` -### 5. **Configure Environment** - -```bash -cp env.example .env -# Edit .env with your PVS IP address -``` - -## ๐Ÿ”ง **VS Code Setup** - -The project includes pre-configured VS Code settings for optimal development: - -### **Extensions (Install these):** +### **Extensions:** - Python (ms-python.python) - Pylance (ms-python.vscode-pylance) - Black Formatter (ms-python.black-formatter) - isort (ms-python.isort) +- flake8 (ms-python.flake8) - GitLens (eamodio.gitlens) ### **Key Features Configured:** From b9cf3189c8f5df127f1d4a62740d719e54ad3cb9 Mon Sep 17 00:00:00 2001 From: Keith Baker Date: Wed, 10 Sep 2025 03:15:50 +0000 Subject: [PATCH 05/10] fix format and fix path of simplejson exception --- custom_components/sunpower/sunpower.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/custom_components/sunpower/sunpower.py b/custom_components/sunpower/sunpower.py index 7db42bc..9fb5a6a 100644 --- a/custom_components/sunpower/sunpower.py +++ b/custom_components/sunpower/sunpower.py @@ -1,4 +1,4 @@ -""" Basic Sunpower PVS Tool """ +"""Basic Sunpower PVS Tool""" import requests import simplejson @@ -25,12 +25,13 @@ def __init__(self, host): def generic_command(self, command): """All 'commands' to the PVS module use this url pattern and return json - The PVS system can take a very long time to respond so timeout is at 2 minutes""" + The PVS system can take a very long time to respond so timeout is at 2 minutes + """ try: return requests.get(self.command_url + command, timeout=120).json() except requests.exceptions.RequestException as error: raise ConnectionException from error - except simplejson.errors.JSONDecodeError as error: + except simplejson.JSONDecodeError as error: raise ParseException from error def device_list(self): @@ -41,12 +42,14 @@ def energy_storage_system_status(self): """Get the status of the energy storage system""" try: return requests.get( - "http://{0}/cgi-bin/dl_cgi/energy-storage-system/status".format(self.host), + "http://{0}/cgi-bin/dl_cgi/energy-storage-system/status".format( + self.host, + ), timeout=120, ).json() except requests.exceptions.RequestException as error: raise ConnectionException from error - except simplejson.errors.JSONDecodeError as error: + except simplejson.JSONDecodeError as error: raise ParseException from error def network_status(self): From 876fec2467f53070a18d98cebd1cd39b38b1edea Mon Sep 17 00:00:00 2001 From: Keith Baker Date: Wed, 10 Sep 2025 04:18:42 +0000 Subject: [PATCH 06/10] Moving towards just pytest Setup mockable PVS fixture Setup marks for types of tests Make test for talking to a real PVS --- pytest.ini | 8 + requirements-dev.txt | 2 + testing/samples/device_list.json | 728 ------------- {testing => tests}/DEBUG_TOOLS_GUIDE.md | 0 {testing => tests}/README.md | 6 +- tests/conftest.py | 43 + {testing => tests}/debug_runner.py | 0 tests/pytest.ini | 8 + tests/samples/device_list.json | 1066 +++++++++++++++++++ tests/samples/ess_device_list.json | 1066 +++++++++++++++++++ tests/samples/ess_status.json | 190 ++++ {testing => tests}/test_api.py | 0 {testing => tests}/test_ha_integration.py | 0 tests/test_pvs.py | 39 + {testing => tests}/test_with_sample_data.py | 0 15 files changed, 2424 insertions(+), 732 deletions(-) create mode 100644 pytest.ini delete mode 100644 testing/samples/device_list.json rename {testing => tests}/DEBUG_TOOLS_GUIDE.md (100%) rename {testing => tests}/README.md (97%) create mode 100644 tests/conftest.py rename {testing => tests}/debug_runner.py (100%) create mode 100644 tests/pytest.ini create mode 100644 tests/samples/device_list.json create mode 100644 tests/samples/ess_device_list.json create mode 100644 tests/samples/ess_status.json rename {testing => tests}/test_api.py (100%) rename {testing => tests}/test_ha_integration.py (100%) create mode 100644 tests/test_pvs.py rename {testing => tests}/test_with_sample_data.py (100%) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..39ddb75 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,8 @@ +[pytest] + +markers = + pvs: Tests that work directly against a pvs to verify its working correctly (not if PVS env == MOCK this will test the Mock) + hass: Tests that validate home assistant functionality + +env = + PVS=MOCK diff --git a/requirements-dev.txt b/requirements-dev.txt index 83eb7f4..1b84d43 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -16,7 +16,9 @@ pre-commit>=3.0.0 # Testing pytest>=7.0.0 pytest-cov +pytest-env pytest-homeassistant-custom-component +pythonping requests simplejson types-requests diff --git a/testing/samples/device_list.json b/testing/samples/device_list.json deleted file mode 100644 index c249cd6..0000000 --- a/testing/samples/device_list.json +++ /dev/null @@ -1,728 +0,0 @@ -{ - "devices": [{ - "DETAIL": "detail", - "STATE": "working", - "STATEDESCR": "Working", - "SERIAL": "ZT204485000549A0321", - "MODEL": "PV Supervisor PVS6", - "HWVER": "6.02", - "SWVER": "2024.2, Build 61640", - "DEVICE_TYPE": "PVS", - "DATATIME": "2024,04,16,23,45,00", - "dl_err_count": "0", - "dl_comm_err": "400", - "dl_skipped_scans": "0", - "dl_scan_time": "1", - "dl_untransmitted": "2487", - "dl_uptime": "99696", - "dl_cpu_load": "0.33", - "dl_mem_used": "86920", - "dl_flash_avail": "104808", - "panid": 3276331684, - "CURTIME": "2024,04,16,23,46,10" - }, { - "ISDETAIL": true, - "SERIAL": "PVS6M20440321p", - "TYPE": "PVS5-METER-P", - "STATE": "working", - "STATEDESCR": "Working", - "MODEL": "PVS6M0400p", - "DESCR": "Power Meter PVS6M20440321p", - "DEVICE_TYPE": "Power Meter", - "interface": "mime", - "production_subtype_enum": "GROSS_PRODUCTION_SITE", - "subtype": "GROSS_PRODUCTION_SITE", - "SWVER": "3000", - "PORT": "", - "DATATIME": "2024,04,16,23,46,10", - "ct_scl_fctr": "50", - "net_ltea_3phsum_kwh": "19327.99", - "p_3phsum_kw": "0", - "q_3phsum_kvar": "0.4392", - "s_3phsum_kva": "0.4492", - "tot_pf_rto": "0", - "freq_hz": "60", - "i_a": "1.8457", - "v12_v": "243.3916", - "CAL0": "50", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,11" - }, { - "ISDETAIL": true, - "SERIAL": "PVS6M20440321c", - "TYPE": "PVS5-METER-C", - "STATE": "working", - "STATEDESCR": "Working", - "MODEL": "PVS6M0400c", - "DESCR": "Power Meter PVS6M20440321c", - "DEVICE_TYPE": "Power Meter", - "interface": "mime", - "consumption_subtype_enum": "GROSS_CONSUMPTION_LINESIDE", - "subtype": "GROSS_CONSUMPTION_LINESIDE", - "SWVER": "3000", - "PORT": "", - "DATATIME": "2024,04,16,23,46,10", - "ct_scl_fctr": "100", - "net_ltea_3phsum_kwh": "27897.1999", - "p_3phsum_kw": "1.3493", - "q_3phsum_kvar": "0.0311", - "s_3phsum_kva": "1.5168", - "tot_pf_rto": "0.8839", - "freq_hz": "60", - "i1_a": "2.2421", - "i2_a": "10.2358", - "v1n_v": "121.9041", - "v2n_v": "121.4879", - "v12_v": "243.3916", - "p1_kw": "0.1869", - "p2_kw": "1.1624", - "neg_ltea_3phsum_kwh": "2.91", - "pos_ltea_3phsum_kwh": "27900.1399", - "CAL0": "100", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,11" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040011392", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040011392", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1328.2679", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.38", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0006", - "v_mppt1_v": "21.97", - "i_mppt1_a": "0.02", - "t_htsnk_degc": "18", - "freq_hz": "60", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,11" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040012744", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040012744", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1391.1325", - "p_3phsum_kw": "0", - "vln_3phavg_v": "244.18", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0006", - "v_mppt1_v": "21.94", - "i_mppt1_a": "0.02", - "t_htsnk_degc": "18", - "freq_hz": "60", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,11" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040013383", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040013383", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1376.6939", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.82", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0003", - "v_mppt1_v": "21.99", - "i_mppt1_a": "0.01", - "t_htsnk_degc": "15", - "freq_hz": "60", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,12" - }, { - "ISDETAIL": true, - "SERIAL": "E002020400144528", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040014528", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1325.1421", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.24", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0006", - "v_mppt1_v": "21.98", - "i_mppt1_a": "0.02", - "t_htsnk_degc": "15", - "freq_hz": "60", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,12" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040016250", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040016250", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1437.9023", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.78", - "i_3phsum_a": "0", - "p_mppt1_kw": "0", - "v_mppt1_v": "21.98", - "i_mppt1_a": "0", - "t_htsnk_degc": "15", - "freq_hz": "60", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,12" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040016476", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040016476", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1366.0335", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.71", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0004", - "v_mppt1_v": "21.98", - "i_mppt1_a": "0.02", - "t_htsnk_degc": "18", - "freq_hz": "60", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,12" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040016406", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040016406", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1380.201", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.76", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0003", - "v_mppt1_v": "21.97", - "i_mppt1_a": "0.01", - "t_htsnk_degc": "18", - "freq_hz": "60", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,12" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040016736", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040016736", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1239.002", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.47", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0017", - "v_mppt1_v": "21.97", - "i_mppt1_a": "0.07", - "t_htsnk_degc": "15", - "freq_hz": "60", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,13" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040016767", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040016767", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1128.181", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.64", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0003", - "v_mppt1_v": "21.99", - "i_mppt1_a": "0.01", - "t_htsnk_degc": "13", - "freq_hz": "60", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,13" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040016936", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040016936", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1163.1473", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.6", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0003", - "v_mppt1_v": "21.98", - "i_mppt1_a": "0.01", - "t_htsnk_degc": "14", - "freq_hz": "60", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,13" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040017146", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040017146", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1402.129", - "p_3phsum_kw": "0", - "vln_3phavg_v": "244.07", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0004", - "v_mppt1_v": "21.98", - "i_mppt1_a": "0.01", - "t_htsnk_degc": "15", - "freq_hz": "60.01", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,13" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040017779", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040017779", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1341.3654", - "p_3phsum_kw": "0", - "vln_3phavg_v": "219.62", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0004", - "v_mppt1_v": "22.01", - "i_mppt1_a": "0.02", - "t_htsnk_degc": "19", - "freq_hz": "60", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,13" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040019106", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040019106", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1207.0696", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.53", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0004", - "v_mppt1_v": "21.96", - "i_mppt1_a": "0.02", - "t_htsnk_degc": "14", - "freq_hz": "60", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,14" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040019210", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040019210", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,28", - "ltea_3phsum_kwh": "1211.5855", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.63", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0002", - "v_mppt1_v": "21.96", - "i_mppt1_a": "0.01", - "t_htsnk_degc": "16", - "freq_hz": "60.01", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,14" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040019664", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040019664", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,43", - "ltea_3phsum_kwh": "1376.5009", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.41", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0005", - "v_mppt1_v": "21.99", - "i_mppt1_a": "0.02", - "t_htsnk_degc": "18", - "freq_hz": "60.01", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,14" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040019050", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040019050", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,43", - "ltea_3phsum_kwh": "1408.4185", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.74", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0009", - "v_mppt1_v": "21.96", - "i_mppt1_a": "0.04", - "t_htsnk_degc": "18", - "freq_hz": "60.01", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,14" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040019099", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040019099", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,43", - "ltea_3phsum_kwh": "1432.2282", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.68", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0006", - "v_mppt1_v": "21.97", - "i_mppt1_a": "0.02", - "t_htsnk_degc": "18", - "freq_hz": "60.01", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,14" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040020435", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040020435", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,43", - "ltea_3phsum_kwh": "1427.6486", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.2", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0008", - "v_mppt1_v": "21.97", - "i_mppt1_a": "0.03", - "t_htsnk_degc": "17", - "freq_hz": "60.01", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,15" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040020589", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040020589", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,43", - "ltea_3phsum_kwh": "1293.9053", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.65", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0003", - "v_mppt1_v": "21.95", - "i_mppt1_a": "0.01", - "t_htsnk_degc": "16", - "freq_hz": "60.01", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,15" - }, { - "ISDETAIL": true, - "SERIAL": "E00202040020677", - "TYPE": "SOLARBRIDGE", - "STATE": "error", - "STATEDESCR": "Error", - "MODEL": "AC_Module_Type_E", - "DESCR": "Inverter E00202040020677", - "DEVICE_TYPE": "Inverter", - "hw_version": "4405", - "interface": "mime", - "module_serial": "", - "PANEL": "SPR-X22-360-E-AC", - "slave": 0, - "SWVER": "4.40.1", - "PORT": "", - "MOD_SN": "", - "NMPLT_SKU": "", - "DATATIME": "2024,04,16,23,27,43", - "ltea_3phsum_kwh": "970.7499", - "p_3phsum_kw": "0", - "vln_3phavg_v": "243.16", - "i_3phsum_a": "0", - "p_mppt1_kw": "0.0006", - "v_mppt1_v": "21.98", - "i_mppt1_a": "0.02", - "t_htsnk_degc": "14", - "freq_hz": "60.01", - "stat_ind": "0", - "origin": "data_logger", - "OPERATION": "noop", - "CURTIME": "2024,04,16,23,46,15" - }], - "result": "succeed" -} diff --git a/testing/DEBUG_TOOLS_GUIDE.md b/tests/DEBUG_TOOLS_GUIDE.md similarity index 100% rename from testing/DEBUG_TOOLS_GUIDE.md rename to tests/DEBUG_TOOLS_GUIDE.md diff --git a/testing/README.md b/tests/README.md similarity index 97% rename from testing/README.md rename to tests/README.md index c713331..852e012 100644 --- a/testing/README.md +++ b/tests/README.md @@ -4,14 +4,12 @@ This directory contains comprehensive testing tools and documentation for the SunPower Home Assistant integration. -## ๐Ÿ“ **Directory Structure** +## ๐Ÿ“ **Tests Directory Structure** ```console -testing/ +tests/ โ”œโ”€โ”€ samples/ โ”‚ โ””โ”€โ”€ device_list.json # Sample PVS data for testing -โ”œโ”€โ”€ docs/ -โ”‚ โ”œโ”€โ”€ DEVELOPMENT_SETUP.md # Complete development setup guide โ”‚ โ””โ”€โ”€ DEBUG_TOOLS_GUIDE.md # Detailed debug tools documentation โ”œโ”€โ”€ test_api.py # Test PVS API connectivity โ”œโ”€โ”€ test_with_sample_data.py # Test integration logic (no hardware) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..dfc5537 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,43 @@ +import json +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "custom_components")) + +import sunpower # noqa: E402 + +DEVICE_LIST_SAMPLE = os.path.join( + os.path.dirname(__file__), + "samples", + "ess_device_list.json", +) +DEVICE_LIST_JSON = json.load(open(DEVICE_LIST_SAMPLE)) +ESS_STATUS_SAMPLE = os.path.join( + os.path.dirname(__file__), + "samples", + "ess_status.json", +) +ESS_STATUS_JSON = json.load(open(ESS_STATUS_SAMPLE)) + + +@pytest.fixture() +def sunpowermonitor(mocker): + """SunPower Monitor fixture.""" + if os.getenv("PVS") == "MOCK": + monitor = sunpower.SunPowerMonitor(None) + mocker.patch.object(monitor, "device_list", return_value=DEVICE_LIST_JSON) + mocker.patch.object( + monitor, + "energy_storage_system_status", + return_value=ESS_STATUS_JSON, + ) + mocker.patch.object(monitor, "network_status", return_value="Something") + yield monitor + return + elif os.getenv("PVS"): + monitor = sunpower.SunPowerMonitor(os.getenv("PVS")) + yield monitor + else: + raise Exception("PVS environment variable not set") diff --git a/testing/debug_runner.py b/tests/debug_runner.py similarity index 100% rename from testing/debug_runner.py rename to tests/debug_runner.py diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..952eeb4 --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,8 @@ +[pytest] + +makers = + pvs: Tests that work directly against a pvs to verify its working correctly (not if PVS env == MOCK this will test the Mock) + hass: Tests that validate home assistant functionality + +env = + PVS: MOCK diff --git a/tests/samples/device_list.json b/tests/samples/device_list.json new file mode 100644 index 0000000..ba00441 --- /dev/null +++ b/tests/samples/device_list.json @@ -0,0 +1,1066 @@ +{ + "devices": [ + { + "DETAIL": "detail", + "STATE": "working", + "STATEDESCR": "Working", + "SERIAL": "ZT1234567890", + "MODEL": "PV Supervisor PVS6", + "HWVER": "6.02", + "SWVER": "2022.7, Build 60661", + "DEVICE_TYPE": "PVS", + "DATATIME": "2024,04,17,18,00,00", + "dl_err_count": "0", + "dl_comm_err": "413", + "dl_skipped_scans": "0", + "dl_scan_time": "73", + "dl_untransmitted": "680553", + "dl_uptime": "36226", + "dl_cpu_load": "0.51", + "dl_mem_used": "87084", + "dl_flash_avail": "28593", + "panid": 410089015, + "CURTIME": "2024,04,17,18,02,19" + }, + { + "ISDETAIL": true, + "SERIAL": "SY12345670-111111.11111", + "TYPE": "HUB+", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "SunPower MIDC", + "DESCR": "HUB+ SY12345670-111111.11111", + "DEVICE_TYPE": "HUB+", + "hw_version": "1.5.0", + "interface": "ttymxc5", + "slave": 220, + "SWVER": "0.7.22", + "PORT": "P0, Modbus, Slave 220", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,20" + }, + { + "ISDETAIL": true, + "SERIAL": "SY111111111D1111", + "TYPE": "EQUINOX-MIO", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "SunPower MIO", + "DESCR": "ESS Hub SY111111111D1111", + "DEVICE_TYPE": "ESS Hub", + "hw_version": "0.4.0", + "interface": "ttymxc5", + "parent": 13, + "slave": 221, + "SWVER": "0.8.5", + "PORT": "P0, Modbus, Slave 221", + "DATATIME": "2024,04,17,18,02,13", + "t_degc": "41", + "humidity": "17", + "v_dcdc_spply_v": "11.397", + "v_spply_v": "11.353", + "v_gateway_v": "11.353", + "fan_actv_fl": "0", + "fw_error": "0", + "event_history": "2048", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,20" + }, + { + "ISDETAIL": true, + "SERIAL": "BC1111111101", + "TYPE": "GATEWAY", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "SchneiderElectric-ConextGateway", + "DESCR": "Gateway BC1111111101", + "DEVICE_TYPE": "Gateway", + "interface": "sunspec", + "mac_address": "40:2e:71:da:c6:47", + "slave": 1, + "SWVER": "V1", + "PORT": "P0, SunSpec, Slave 1", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,20" + }, + { + "ISDETAIL": true, + "SERIAL": "1111111111", + "TYPE": "SCHNEIDER-XWPRO", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "SchneiderElectric-XW6848-21", + "DESCR": "Storage Inverter 1111111111", + "DEVICE_TYPE": "Storage Inverter", + "interface": "sunspec", + "mac_address": "40:2e:71:da:c6:47", + "parent": 13, + "slave": 10, + "SWVER": "V1", + "PORT": "P0, SunSpec, Slave 10", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,20" + }, + { + "ISDETAIL": true, + "SERIAL": "BC111111111111111111", + "TYPE": "EQUINOX-BMS", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "SchneiderElectric-SP1", + "DESCR": "ESS BMS BC111111111111111111", + "DEVICE_TYPE": "ESS BMS", + "interface": "sunspec", + "mac_address": "40:2e:71:da:c6:00", + "parent": 13, + "slave": 230, + "PORT": "P0, SunSpec, Slave 230", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,21" + }, + { + "ISDETAIL": true, + "SERIAL": "PVS6M11111111p", + "TYPE": "PVS5-METER-P", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "PVS6M0400p", + "DESCR": "Power Meter PVS6M11111111p", + "DEVICE_TYPE": "Power Meter", + "interface": "mime", + "subtype": "GROSS_PRODUCTION_SITE", + "SWVER": "3000", + "PORT": "", + "DATATIME": "2024,04,17,18,02,20", + "ct_scl_fctr": "50", + "net_ltea_3phsum_kwh": "28817.2799", + "p_3phsum_kw": "5.8049", + "q_3phsum_kvar": "-0.1899", + "s_3phsum_kva": "5.8092", + "tot_pf_rto": "0.9991", + "freq_hz": "60", + "CAL0": "50", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,21" + }, + { + "ISDETAIL": true, + "SERIAL": "PVS6M11111111c", + "TYPE": "PVS5-METER-C", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "PVS6M0400c", + "DESCR": "Power Meter PVS6M11111111c", + "DEVICE_TYPE": "Power Meter", + "interface": "mime", + "subtype": "NET_CONSUMPTION_LOADSIDE", + "SWVER": "3000", + "PORT": "", + "DATATIME": "2024,04,17,18,02,21", + "ct_scl_fctr": "100", + "net_ltea_3phsum_kwh": "18037.2", + "p_3phsum_kw": "0.0727", + "q_3phsum_kvar": "0.3072", + "s_3phsum_kva": "0.6905", + "tot_pf_rto": "-0.0775", + "freq_hz": "60", + "i1_a": "2.9878", + "i2_a": "2.6403", + "v1n_v": "122.9457", + "v2n_v": "122.419", + "v12_v": "245.3642", + "p1_kw": "-0.19", + "p2_kw": "0.2628", + "neg_ltea_3phsum_kwh": "8442.2099", + "pos_ltea_3phsum_kwh": "26479.36", + "CAL0": "100", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,21" + }, + { + "ISDETAIL": true, + "SERIAL": "SY1111111-111111.11111_PVD1", + "TYPE": "PV-DISCONNECT", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "SPWR-PVD-model", + "DESCR": "PV Disconnect SY1111111-111111.11111_PVD1", + "DEVICE_TYPE": "PV Disconnect", + "hw_version": "0", + "interface": "none", + "slave": 0, + "SWVER": "0", + "PORT": "P0, Modbus, Slave 0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,21" + }, + { + "ISDETAIL": true, + "SERIAL": "M111111111111", + "TYPE": "BATTERY", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "POWERAMP-Komodo 1.2", + "DESCR": "Battery M111111111111", + "DEVICE_TYPE": "Battery", + "hw_version": "4.33", + "interface": "none", + "parent": 13, + "SWVER": "2.8", + "PORT": "P0, None, Slave -1", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,22" + }, + { + "ISDETAIL": true, + "SERIAL": "M111111111112", + "TYPE": "BATTERY", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "POWERAMP-Komodo 1.2", + "DESCR": "Battery M111111111112", + "DEVICE_TYPE": "Battery", + "hw_version": "4.33", + "interface": "none", + "parent": 13, + "SWVER": "2.8", + "PORT": "P0, None, Slave -1", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,22" + }, + { + "ISDETAIL": true, + "SERIAL": "M111111111113", + "TYPE": "BATTERY", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "POWERAMP-Komodo 1.2", + "DESCR": "Battery M111111111113", + "DEVICE_TYPE": "Battery", + "hw_version": "4.33", + "interface": "none", + "parent": 13, + "SWVER": "2.8", + "PORT": "P0, None, Slave -1", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,22" + }, + { + "ISDETAIL": true, + "SERIAL": "M111111111114", + "TYPE": "BATTERY", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "POWERAMP-Komodo 1.2", + "DESCR": "Battery M111111111114", + "DEVICE_TYPE": "Battery", + "hw_version": "4.33", + "interface": "none", + "parent": 13, + "SWVER": "2.8", + "PORT": "P0, None, Slave -1", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,23" + }, + { + "ISDETAIL": true, + "SERIAL": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "TYPE": "EQUINOX-ESS", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "SPWR-Equinox-model", + "DESCR": "Energy Storage System 00001C2C9E04_M001220470180F_M001220470180", + "DEVICE_TYPE": "Energy Storage System", + "hw_version": "0", + "interface": "none", + "operational_ac_kW": 6, + "operational_ac_kWh": 21.96, + "rated_ac_kW": 6.8, + "rated_ac_kWh": 26, + "SWVER": "0", + "PORT": "P0, Parent, Slave -1", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,23" + }, + { + "ISDETAIL": true, + "SERIAL": "E222222222222", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E222222222222", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "Q23M20535790", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "Q23M20535790", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1986.214", + "p_3phsum_kw": "0.2334", + "vln_3phavg_v": "249.73", + "i_3phsum_a": "0.93", + "p_mppt1_kw": "0.3063", + "v_mppt1_v": "53.13", + "i_mppt1_a": "5.76", + "t_htsnk_degc": "44", + "freq_hz": "59.99", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,23" + }, + { + "ISDETAIL": true, + "SERIAL": "E222222222223", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E222222222223", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "Q23111111111", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "Q23111111111", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1905.5391", + "p_3phsum_kw": "0.2275", + "vln_3phavg_v": "249.46", + "i_3phsum_a": "0.91", + "p_mppt1_kw": "0.3083", + "v_mppt1_v": "53.24", + "i_mppt1_a": "5.79", + "t_htsnk_degc": "46", + "freq_hz": "59.99", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,23" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901123", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901123", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345678901123", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901123", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "2211.4533", + "p_3phsum_kw": "0.1955", + "vln_3phavg_v": "247.1", + "i_3phsum_a": "0.79", + "p_mppt1_kw": "0.2599", + "v_mppt1_v": "54.1", + "i_mppt1_a": "4.8", + "t_htsnk_degc": "51", + "freq_hz": "59.99", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,23" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901124", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901124", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345678901124", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901124", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1876.7503", + "p_3phsum_kw": "0.2275", + "vln_3phavg_v": "248.62", + "i_3phsum_a": "0.91", + "p_mppt1_kw": "0.3038", + "v_mppt1_v": "53.21", + "i_mppt1_a": "5.71", + "t_htsnk_degc": "47", + "freq_hz": "60", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,24" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901125", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901125", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E1234567890115", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901155", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1905.0786", + "p_3phsum_kw": "0.2289", + "vln_3phavg_v": "248.69", + "i_3phsum_a": "0.92", + "p_mppt1_kw": "0.2957", + "v_mppt1_v": "52.75", + "i_mppt1_a": "5.6", + "t_htsnk_degc": "46", + "freq_hz": "60", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,24" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901125", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901125", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345678901", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "2191.7205", + "p_3phsum_kw": "0.1982", + "vln_3phavg_v": "247.3", + "i_3phsum_a": "0.8", + "p_mppt1_kw": "0.2556", + "v_mppt1_v": "54.65", + "i_mppt1_a": "4.67", + "t_htsnk_degc": "37", + "freq_hz": "60", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,24" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901126", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901126", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456789011", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1923.3012", + "p_3phsum_kw": "0.2347", + "vln_3phavg_v": "249.15", + "i_3phsum_a": "0.94", + "p_mppt1_kw": "0.298", + "v_mppt1_v": "53.19", + "i_mppt1_a": "5.6", + "t_htsnk_degc": "47", + "freq_hz": "60", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,24" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901127", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901127", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E1901124", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E1901124", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1909.901", + "p_3phsum_kw": "0.2341", + "vln_3phavg_v": "248.62", + "i_3phsum_a": "0.94", + "p_mppt1_kw": "0.3021", + "v_mppt1_v": "53.35", + "i_mppt1_a": "5.66", + "t_htsnk_degc": "46", + "freq_hz": "60.01", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,24" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901128", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901128", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123401127", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123401127", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1906.5346", + "p_3phsum_kw": "0.2337", + "vln_3phavg_v": "249.01", + "i_3phsum_a": "0.93", + "p_mppt1_kw": "0.3048", + "v_mppt1_v": "53.1", + "i_mppt1_a": "5.74", + "t_htsnk_degc": "46", + "freq_hz": "60.01", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,25" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901129", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901129", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123451128", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123451128", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1941.8943", + "p_3phsum_kw": "0.2349", + "vln_3phavg_v": "249.2", + "i_3phsum_a": "0.94", + "p_mppt1_kw": "0.3071", + "v_mppt1_v": "52.55", + "i_mppt1_a": "5.84", + "t_htsnk_degc": "48", + "freq_hz": "60.01", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,25" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901130", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901130", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456130", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456130", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1892.0793", + "p_3phsum_kw": "0.2341", + "vln_3phavg_v": "248.7", + "i_3phsum_a": "0.94", + "p_mppt1_kw": "0.3163", + "v_mppt1_v": "53.21", + "i_mppt1_a": "5.94", + "t_htsnk_degc": "46", + "freq_hz": "60.01", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,25" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901131", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901131", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345630", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345630", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "2171.8629", + "p_3phsum_kw": "0.2014", + "vln_3phavg_v": "247.67", + "i_3phsum_a": "0.81", + "p_mppt1_kw": "0.2688", + "v_mppt1_v": "54.76", + "i_mppt1_a": "4.9", + "t_htsnk_degc": "35", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,25" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901132", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901132", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456320", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456320", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1948.6541", + "p_3phsum_kw": "0.2364", + "vln_3phavg_v": "249.48", + "i_3phsum_a": "0.94", + "p_mppt1_kw": "0.3116", + "v_mppt1_v": "52.93", + "i_mppt1_a": "5.88", + "t_htsnk_degc": "47", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,25" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901133", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901133", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456321", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456321", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "2238.8076", + "p_3phsum_kw": "0.2071", + "vln_3phavg_v": "247.4", + "i_3phsum_a": "0.83", + "p_mppt1_kw": "0.2702", + "v_mppt1_v": "54.89", + "i_mppt1_a": "4.92", + "t_htsnk_degc": "34", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,26" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901134", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901134", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456322", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456322", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "2163.6433", + "p_3phsum_kw": "0.2009", + "vln_3phavg_v": "247.53", + "i_3phsum_a": "0.81", + "p_mppt1_kw": "0.2626", + "v_mppt1_v": "54.39", + "i_mppt1_a": "4.82", + "t_htsnk_degc": "38", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,26" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901135", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901135", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456323", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456324", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "1954.5777", + "p_3phsum_kw": "0.2424", + "vln_3phavg_v": "250.1", + "i_3phsum_a": "0.96", + "p_mppt1_kw": "0.3116", + "v_mppt1_v": "53.45", + "i_mppt1_a": "5.83", + "t_htsnk_degc": "48", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,26" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901136", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901136", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456323", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456323", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "1881.1138", + "p_3phsum_kw": "0.2365", + "vln_3phavg_v": "249.83", + "i_3phsum_a": "0.94", + "p_mppt1_kw": "0.3067", + "v_mppt1_v": "53.22", + "i_mppt1_a": "5.76", + "t_htsnk_degc": "46", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,26" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901137", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901137", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456324", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456324", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "2192.3515", + "p_3phsum_kw": "0.205", + "vln_3phavg_v": "247.64", + "i_3phsum_a": "0.82", + "p_mppt1_kw": "0.2647", + "v_mppt1_v": "54.46", + "i_mppt1_a": "4.86", + "t_htsnk_degc": "38", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,27" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901138", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901138", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456324", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456324", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "1972.373", + "p_3phsum_kw": "0.2463", + "vln_3phavg_v": "248.97", + "i_3phsum_a": "0.98", + "p_mppt1_kw": "0.3139", + "v_mppt1_v": "53.86", + "i_mppt1_a": "5.82", + "t_htsnk_degc": "43", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,27" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901139", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901139", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456325", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456325", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "2186.3743", + "p_3phsum_kw": "0.2046", + "vln_3phavg_v": "247.34", + "i_3phsum_a": "0.82", + "p_mppt1_kw": "0.2618", + "v_mppt1_v": "54.57", + "i_mppt1_a": "4.79", + "t_htsnk_degc": "38", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,27" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901140", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901140", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345678901140", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901140", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "1938.8079", + "p_3phsum_kw": "0.2437", + "vln_3phavg_v": "249.9", + "i_3phsum_a": "0.97", + "p_mppt1_kw": "0.309", + "v_mppt1_v": "53.48", + "i_mppt1_a": "5.77", + "t_htsnk_degc": "46", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,27" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901141", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901141", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345678901141", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901141", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "1883.5216", + "p_3phsum_kw": "0.2437", + "vln_3phavg_v": "249.2", + "i_3phsum_a": "0.97", + "p_mppt1_kw": "0.3128", + "v_mppt1_v": "53.59", + "i_mppt1_a": "5.83", + "t_htsnk_degc": "46", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,27" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901142", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901142", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345678901141", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901141", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "1931.6134", + "p_3phsum_kw": "0.2454", + "vln_3phavg_v": "249.52", + "i_3phsum_a": "0.98", + "p_mppt1_kw": "0.3053", + "v_mppt1_v": "53.56", + "i_mppt1_a": "5.7", + "t_htsnk_degc": "43", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,28" + } + ], + "result": "succeed" +} diff --git a/tests/samples/ess_device_list.json b/tests/samples/ess_device_list.json new file mode 100644 index 0000000..ba00441 --- /dev/null +++ b/tests/samples/ess_device_list.json @@ -0,0 +1,1066 @@ +{ + "devices": [ + { + "DETAIL": "detail", + "STATE": "working", + "STATEDESCR": "Working", + "SERIAL": "ZT1234567890", + "MODEL": "PV Supervisor PVS6", + "HWVER": "6.02", + "SWVER": "2022.7, Build 60661", + "DEVICE_TYPE": "PVS", + "DATATIME": "2024,04,17,18,00,00", + "dl_err_count": "0", + "dl_comm_err": "413", + "dl_skipped_scans": "0", + "dl_scan_time": "73", + "dl_untransmitted": "680553", + "dl_uptime": "36226", + "dl_cpu_load": "0.51", + "dl_mem_used": "87084", + "dl_flash_avail": "28593", + "panid": 410089015, + "CURTIME": "2024,04,17,18,02,19" + }, + { + "ISDETAIL": true, + "SERIAL": "SY12345670-111111.11111", + "TYPE": "HUB+", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "SunPower MIDC", + "DESCR": "HUB+ SY12345670-111111.11111", + "DEVICE_TYPE": "HUB+", + "hw_version": "1.5.0", + "interface": "ttymxc5", + "slave": 220, + "SWVER": "0.7.22", + "PORT": "P0, Modbus, Slave 220", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,20" + }, + { + "ISDETAIL": true, + "SERIAL": "SY111111111D1111", + "TYPE": "EQUINOX-MIO", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "SunPower MIO", + "DESCR": "ESS Hub SY111111111D1111", + "DEVICE_TYPE": "ESS Hub", + "hw_version": "0.4.0", + "interface": "ttymxc5", + "parent": 13, + "slave": 221, + "SWVER": "0.8.5", + "PORT": "P0, Modbus, Slave 221", + "DATATIME": "2024,04,17,18,02,13", + "t_degc": "41", + "humidity": "17", + "v_dcdc_spply_v": "11.397", + "v_spply_v": "11.353", + "v_gateway_v": "11.353", + "fan_actv_fl": "0", + "fw_error": "0", + "event_history": "2048", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,20" + }, + { + "ISDETAIL": true, + "SERIAL": "BC1111111101", + "TYPE": "GATEWAY", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "SchneiderElectric-ConextGateway", + "DESCR": "Gateway BC1111111101", + "DEVICE_TYPE": "Gateway", + "interface": "sunspec", + "mac_address": "40:2e:71:da:c6:47", + "slave": 1, + "SWVER": "V1", + "PORT": "P0, SunSpec, Slave 1", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,20" + }, + { + "ISDETAIL": true, + "SERIAL": "1111111111", + "TYPE": "SCHNEIDER-XWPRO", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "SchneiderElectric-XW6848-21", + "DESCR": "Storage Inverter 1111111111", + "DEVICE_TYPE": "Storage Inverter", + "interface": "sunspec", + "mac_address": "40:2e:71:da:c6:47", + "parent": 13, + "slave": 10, + "SWVER": "V1", + "PORT": "P0, SunSpec, Slave 10", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,20" + }, + { + "ISDETAIL": true, + "SERIAL": "BC111111111111111111", + "TYPE": "EQUINOX-BMS", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "SchneiderElectric-SP1", + "DESCR": "ESS BMS BC111111111111111111", + "DEVICE_TYPE": "ESS BMS", + "interface": "sunspec", + "mac_address": "40:2e:71:da:c6:00", + "parent": 13, + "slave": 230, + "PORT": "P0, SunSpec, Slave 230", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,21" + }, + { + "ISDETAIL": true, + "SERIAL": "PVS6M11111111p", + "TYPE": "PVS5-METER-P", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "PVS6M0400p", + "DESCR": "Power Meter PVS6M11111111p", + "DEVICE_TYPE": "Power Meter", + "interface": "mime", + "subtype": "GROSS_PRODUCTION_SITE", + "SWVER": "3000", + "PORT": "", + "DATATIME": "2024,04,17,18,02,20", + "ct_scl_fctr": "50", + "net_ltea_3phsum_kwh": "28817.2799", + "p_3phsum_kw": "5.8049", + "q_3phsum_kvar": "-0.1899", + "s_3phsum_kva": "5.8092", + "tot_pf_rto": "0.9991", + "freq_hz": "60", + "CAL0": "50", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,21" + }, + { + "ISDETAIL": true, + "SERIAL": "PVS6M11111111c", + "TYPE": "PVS5-METER-C", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "PVS6M0400c", + "DESCR": "Power Meter PVS6M11111111c", + "DEVICE_TYPE": "Power Meter", + "interface": "mime", + "subtype": "NET_CONSUMPTION_LOADSIDE", + "SWVER": "3000", + "PORT": "", + "DATATIME": "2024,04,17,18,02,21", + "ct_scl_fctr": "100", + "net_ltea_3phsum_kwh": "18037.2", + "p_3phsum_kw": "0.0727", + "q_3phsum_kvar": "0.3072", + "s_3phsum_kva": "0.6905", + "tot_pf_rto": "-0.0775", + "freq_hz": "60", + "i1_a": "2.9878", + "i2_a": "2.6403", + "v1n_v": "122.9457", + "v2n_v": "122.419", + "v12_v": "245.3642", + "p1_kw": "-0.19", + "p2_kw": "0.2628", + "neg_ltea_3phsum_kwh": "8442.2099", + "pos_ltea_3phsum_kwh": "26479.36", + "CAL0": "100", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,21" + }, + { + "ISDETAIL": true, + "SERIAL": "SY1111111-111111.11111_PVD1", + "TYPE": "PV-DISCONNECT", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "SPWR-PVD-model", + "DESCR": "PV Disconnect SY1111111-111111.11111_PVD1", + "DEVICE_TYPE": "PV Disconnect", + "hw_version": "0", + "interface": "none", + "slave": 0, + "SWVER": "0", + "PORT": "P0, Modbus, Slave 0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,21" + }, + { + "ISDETAIL": true, + "SERIAL": "M111111111111", + "TYPE": "BATTERY", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "POWERAMP-Komodo 1.2", + "DESCR": "Battery M111111111111", + "DEVICE_TYPE": "Battery", + "hw_version": "4.33", + "interface": "none", + "parent": 13, + "SWVER": "2.8", + "PORT": "P0, None, Slave -1", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,22" + }, + { + "ISDETAIL": true, + "SERIAL": "M111111111112", + "TYPE": "BATTERY", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "POWERAMP-Komodo 1.2", + "DESCR": "Battery M111111111112", + "DEVICE_TYPE": "Battery", + "hw_version": "4.33", + "interface": "none", + "parent": 13, + "SWVER": "2.8", + "PORT": "P0, None, Slave -1", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,22" + }, + { + "ISDETAIL": true, + "SERIAL": "M111111111113", + "TYPE": "BATTERY", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "POWERAMP-Komodo 1.2", + "DESCR": "Battery M111111111113", + "DEVICE_TYPE": "Battery", + "hw_version": "4.33", + "interface": "none", + "parent": 13, + "SWVER": "2.8", + "PORT": "P0, None, Slave -1", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,22" + }, + { + "ISDETAIL": true, + "SERIAL": "M111111111114", + "TYPE": "BATTERY", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "POWERAMP-Komodo 1.2", + "DESCR": "Battery M111111111114", + "DEVICE_TYPE": "Battery", + "hw_version": "4.33", + "interface": "none", + "parent": 13, + "SWVER": "2.8", + "PORT": "P0, None, Slave -1", + "origin": "data_logger", + "OPERATION": "noop", + "PARENT": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "CURTIME": "2024,04,17,18,02,23" + }, + { + "ISDETAIL": true, + "SERIAL": "111111111111_111111111111F_1111111111110E_111111111111F_1111111111113D", + "TYPE": "EQUINOX-ESS", + "STATE": "error", + "STATEDESCR": "Error", + "MODEL": "SPWR-Equinox-model", + "DESCR": "Energy Storage System 00001C2C9E04_M001220470180F_M001220470180", + "DEVICE_TYPE": "Energy Storage System", + "hw_version": "0", + "interface": "none", + "operational_ac_kW": 6, + "operational_ac_kWh": 21.96, + "rated_ac_kW": 6.8, + "rated_ac_kWh": 26, + "SWVER": "0", + "PORT": "P0, Parent, Slave -1", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,23" + }, + { + "ISDETAIL": true, + "SERIAL": "E222222222222", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E222222222222", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "Q23M20535790", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "Q23M20535790", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1986.214", + "p_3phsum_kw": "0.2334", + "vln_3phavg_v": "249.73", + "i_3phsum_a": "0.93", + "p_mppt1_kw": "0.3063", + "v_mppt1_v": "53.13", + "i_mppt1_a": "5.76", + "t_htsnk_degc": "44", + "freq_hz": "59.99", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,23" + }, + { + "ISDETAIL": true, + "SERIAL": "E222222222223", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E222222222223", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "Q23111111111", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "Q23111111111", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1905.5391", + "p_3phsum_kw": "0.2275", + "vln_3phavg_v": "249.46", + "i_3phsum_a": "0.91", + "p_mppt1_kw": "0.3083", + "v_mppt1_v": "53.24", + "i_mppt1_a": "5.79", + "t_htsnk_degc": "46", + "freq_hz": "59.99", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,23" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901123", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901123", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345678901123", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901123", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "2211.4533", + "p_3phsum_kw": "0.1955", + "vln_3phavg_v": "247.1", + "i_3phsum_a": "0.79", + "p_mppt1_kw": "0.2599", + "v_mppt1_v": "54.1", + "i_mppt1_a": "4.8", + "t_htsnk_degc": "51", + "freq_hz": "59.99", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,23" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901124", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901124", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345678901124", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901124", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1876.7503", + "p_3phsum_kw": "0.2275", + "vln_3phavg_v": "248.62", + "i_3phsum_a": "0.91", + "p_mppt1_kw": "0.3038", + "v_mppt1_v": "53.21", + "i_mppt1_a": "5.71", + "t_htsnk_degc": "47", + "freq_hz": "60", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,24" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901125", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901125", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E1234567890115", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901155", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1905.0786", + "p_3phsum_kw": "0.2289", + "vln_3phavg_v": "248.69", + "i_3phsum_a": "0.92", + "p_mppt1_kw": "0.2957", + "v_mppt1_v": "52.75", + "i_mppt1_a": "5.6", + "t_htsnk_degc": "46", + "freq_hz": "60", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,24" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901125", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901125", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345678901", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "2191.7205", + "p_3phsum_kw": "0.1982", + "vln_3phavg_v": "247.3", + "i_3phsum_a": "0.8", + "p_mppt1_kw": "0.2556", + "v_mppt1_v": "54.65", + "i_mppt1_a": "4.67", + "t_htsnk_degc": "37", + "freq_hz": "60", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,24" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901126", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901126", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456789011", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1923.3012", + "p_3phsum_kw": "0.2347", + "vln_3phavg_v": "249.15", + "i_3phsum_a": "0.94", + "p_mppt1_kw": "0.298", + "v_mppt1_v": "53.19", + "i_mppt1_a": "5.6", + "t_htsnk_degc": "47", + "freq_hz": "60", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,24" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901127", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901127", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E1901124", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E1901124", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1909.901", + "p_3phsum_kw": "0.2341", + "vln_3phavg_v": "248.62", + "i_3phsum_a": "0.94", + "p_mppt1_kw": "0.3021", + "v_mppt1_v": "53.35", + "i_mppt1_a": "5.66", + "t_htsnk_degc": "46", + "freq_hz": "60.01", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,24" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901128", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901128", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123401127", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123401127", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1906.5346", + "p_3phsum_kw": "0.2337", + "vln_3phavg_v": "249.01", + "i_3phsum_a": "0.93", + "p_mppt1_kw": "0.3048", + "v_mppt1_v": "53.1", + "i_mppt1_a": "5.74", + "t_htsnk_degc": "46", + "freq_hz": "60.01", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,25" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901129", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901129", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123451128", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123451128", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1941.8943", + "p_3phsum_kw": "0.2349", + "vln_3phavg_v": "249.2", + "i_3phsum_a": "0.94", + "p_mppt1_kw": "0.3071", + "v_mppt1_v": "52.55", + "i_mppt1_a": "5.84", + "t_htsnk_degc": "48", + "freq_hz": "60.01", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,25" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901130", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901130", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456130", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456130", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1892.0793", + "p_3phsum_kw": "0.2341", + "vln_3phavg_v": "248.7", + "i_3phsum_a": "0.94", + "p_mppt1_kw": "0.3163", + "v_mppt1_v": "53.21", + "i_mppt1_a": "5.94", + "t_htsnk_degc": "46", + "freq_hz": "60.01", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,25" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901131", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901131", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345630", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345630", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "2171.8629", + "p_3phsum_kw": "0.2014", + "vln_3phavg_v": "247.67", + "i_3phsum_a": "0.81", + "p_mppt1_kw": "0.2688", + "v_mppt1_v": "54.76", + "i_mppt1_a": "4.9", + "t_htsnk_degc": "35", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,25" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901132", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901132", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456320", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456320", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "1948.6541", + "p_3phsum_kw": "0.2364", + "vln_3phavg_v": "249.48", + "i_3phsum_a": "0.94", + "p_mppt1_kw": "0.3116", + "v_mppt1_v": "52.93", + "i_mppt1_a": "5.88", + "t_htsnk_degc": "47", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,25" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901133", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901133", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456321", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456321", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "2238.8076", + "p_3phsum_kw": "0.2071", + "vln_3phavg_v": "247.4", + "i_3phsum_a": "0.83", + "p_mppt1_kw": "0.2702", + "v_mppt1_v": "54.89", + "i_mppt1_a": "4.92", + "t_htsnk_degc": "34", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,26" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901134", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901134", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456322", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456322", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,43", + "ltea_3phsum_kwh": "2163.6433", + "p_3phsum_kw": "0.2009", + "vln_3phavg_v": "247.53", + "i_3phsum_a": "0.81", + "p_mppt1_kw": "0.2626", + "v_mppt1_v": "54.39", + "i_mppt1_a": "4.82", + "t_htsnk_degc": "38", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,26" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901135", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901135", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456323", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456324", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "1954.5777", + "p_3phsum_kw": "0.2424", + "vln_3phavg_v": "250.1", + "i_3phsum_a": "0.96", + "p_mppt1_kw": "0.3116", + "v_mppt1_v": "53.45", + "i_mppt1_a": "5.83", + "t_htsnk_degc": "48", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,26" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901136", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901136", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456323", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456323", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "1881.1138", + "p_3phsum_kw": "0.2365", + "vln_3phavg_v": "249.83", + "i_3phsum_a": "0.94", + "p_mppt1_kw": "0.3067", + "v_mppt1_v": "53.22", + "i_mppt1_a": "5.76", + "t_htsnk_degc": "46", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,26" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901137", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901137", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456324", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456324", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "2192.3515", + "p_3phsum_kw": "0.205", + "vln_3phavg_v": "247.64", + "i_3phsum_a": "0.82", + "p_mppt1_kw": "0.2647", + "v_mppt1_v": "54.46", + "i_mppt1_a": "4.86", + "t_htsnk_degc": "38", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,27" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901138", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901138", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456324", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456324", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "1972.373", + "p_3phsum_kw": "0.2463", + "vln_3phavg_v": "248.97", + "i_3phsum_a": "0.98", + "p_mppt1_kw": "0.3139", + "v_mppt1_v": "53.86", + "i_mppt1_a": "5.82", + "t_htsnk_degc": "43", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,27" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901139", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901139", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E123456325", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E123456325", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "2186.3743", + "p_3phsum_kw": "0.2046", + "vln_3phavg_v": "247.34", + "i_3phsum_a": "0.82", + "p_mppt1_kw": "0.2618", + "v_mppt1_v": "54.57", + "i_mppt1_a": "4.79", + "t_htsnk_degc": "38", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,27" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901140", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901140", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345678901140", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901140", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "1938.8079", + "p_3phsum_kw": "0.2437", + "vln_3phavg_v": "249.9", + "i_3phsum_a": "0.97", + "p_mppt1_kw": "0.309", + "v_mppt1_v": "53.48", + "i_mppt1_a": "5.77", + "t_htsnk_degc": "46", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,27" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901141", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901141", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345678901141", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901141", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "1883.5216", + "p_3phsum_kw": "0.2437", + "vln_3phavg_v": "249.2", + "i_3phsum_a": "0.97", + "p_mppt1_kw": "0.3128", + "v_mppt1_v": "53.59", + "i_mppt1_a": "5.83", + "t_htsnk_degc": "46", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,27" + }, + { + "ISDETAIL": true, + "SERIAL": "E12345678901142", + "TYPE": "SOLARBRIDGE", + "STATE": "working", + "STATEDESCR": "Working", + "MODEL": "AC_Module_Type_E", + "DESCR": "Inverter E12345678901142", + "DEVICE_TYPE": "Inverter", + "hw_version": "4405", + "interface": "mime", + "module_serial": "E12345678901141", + "PANEL": "SPR-X21-335-BLK-E-AC", + "slave": 0, + "SWVER": "4.28.7", + "PORT": "", + "MOD_SN": "E12345678901141", + "NMPLT_SKU": "", + "DATATIME": "2024,04,17,18,00,58", + "ltea_3phsum_kwh": "1931.6134", + "p_3phsum_kw": "0.2454", + "vln_3phavg_v": "249.52", + "i_3phsum_a": "0.98", + "p_mppt1_kw": "0.3053", + "v_mppt1_v": "53.56", + "i_mppt1_a": "5.7", + "t_htsnk_degc": "43", + "freq_hz": "60.02", + "stat_ind": "0", + "origin": "data_logger", + "OPERATION": "noop", + "CURTIME": "2024,04,17,18,02,28" + } + ], + "result": "succeed" +} diff --git a/tests/samples/ess_status.json b/tests/samples/ess_status.json new file mode 100644 index 0000000..049fb09 --- /dev/null +++ b/tests/samples/ess_status.json @@ -0,0 +1,190 @@ +{ + "errors": [ + { + "device_sn": "111111111111", + "error_code": "12001", + "error_message": "storage_inv_over_temperature_warning", + "error_name": "storage_inv_over_temperature_warning", + "last_occurence": "2024-04-17 18:01:22", + "value": { + "unit": "", + "value": 51.15 + } + } + ], + "ess_report": { + "battery_status": [ + { + "battery_amperage": { + "unit": "A", + "value": 8.4 + }, + "battery_voltage": { + "unit": "V", + "value": 53.2 + }, + "customer_state_of_charge": { + "unit": "%", + "value": 55.000000000000007 + }, + "last_updated": "2024-04-17 18:01:13", + "serial_number": "BC1111111111111111111", + "system_state_of_charge": { + "unit": "%", + "value": 62 + }, + "temperature": { + "unit": "C", + "value": 29.8 + } + } + ], + "ess_state": [ + { + "operational_mode": "SELF_CONSUMPTION", + "permission_to_operate": true, + "storage_controller_status": "RUNNING" + } + ], + "ess_status": [ + { + "enclosure_humidity": { + "unit": "%", + "value": 17 + }, + "enclosure_temperature": { + "unit": "C", + "value": 41 + }, + "ess_meter_reading": { + "agg_power": { + "unit": "kW", + "value": -4.962 + }, + "last_updated": "2024-04-17 18:01:22", + "meter_a": { + "reading": { + "current": { + "unit": "A", + "value": 17.78 + }, + "last_updated": "2024-04-17 18:01:22", + "power": { + "unit": "W", + "value": 2164.3594000000003 + }, + "voltage": { + "unit": "V", + "value": 121.73 + } + } + }, + "meter_b": { + "reading": { + "current": { + "unit": "A", + "value": 17.82 + }, + "last_updated": "2024-04-17 18:01:22", + "power": { + "unit": "W", + "value": 2169.2286 + }, + "voltage": { + "unit": "V", + "value": 121.73 + } + } + } + }, + "last_updated": "2024-04-17 18:01:13", + "serial_number": "11111111111_M1111111111F_M1111111111111111E_M1111111111111F_M111111111111D" + } + ], + "hub_plus_status": { + "aux_port_voltage": { + "unit": "V", + "value": 11.276 + }, + "contactor_error": "NONE", + "contactor_position": "CLOSED", + "grid_frequency_state": "METER_FREQ_IN_RANGE", + "grid_phase1_voltage": { + "unit": "V", + "value": 123.30000000000001 + }, + "grid_phase2_voltage": { + "unit": "V", + "value": 123.2 + }, + "grid_voltage_state": "METER_VOLTAGE_IN_RANGE", + "hub_humidity": { + "unit": "%", + "value": 33 + }, + "hub_temperature": { + "unit": "C", + "value": 26 + }, + "inverter_connection_voltage": { + "unit": "V", + "value": 0.277 + }, + "jump_start_voltage": { + "unit": "V", + "value": 1.177 + }, + "last_updated": "2024-04-17 18:01:20", + "load_frequency_state": "METER_FREQ_IN_RANGE", + "load_phase1_voltage": { + "unit": "V", + "value": 123.2 + }, + "load_phase2_voltage": { + "unit": "V", + "value": 123.30000000000001 + }, + "load_voltage_state": "METER_VOLTAGE_IN_RANGE", + "main_voltage": { + "unit": "V", + "value": 11.364 + }, + "serial_number": "SY1234567-11111111.111111" + }, + "inverter_status": [ + { + "a_n_voltage": { + "unit": "V", + "value": 121.73 + }, + "ac_current": { + "unit": "A", + "value": 17.8 + }, + "ac_power": { + "unit": "kW", + "value": -4.962 + }, + "b_n_voltage": { + "unit": "V", + "value": 121.73 + }, + "last_updated": "2024-04-17 18:01:22", + "phase_a_current": { + "unit": "A", + "value": 17.78 + }, + "phase_b_current": { + "unit": "A", + "value": 17.82 + }, + "serial_number": "111111111111", + "temperature": { + "unit": "C", + "value": 52.550000000000004 + } + } + ], + "last_updated": "2024-04-17 18:01:22" + } +} diff --git a/testing/test_api.py b/tests/test_api.py similarity index 100% rename from testing/test_api.py rename to tests/test_api.py diff --git a/testing/test_ha_integration.py b/tests/test_ha_integration.py similarity index 100% rename from testing/test_ha_integration.py rename to tests/test_ha_integration.py diff --git a/tests/test_pvs.py b/tests/test_pvs.py new file mode 100644 index 0000000..6a42340 --- /dev/null +++ b/tests/test_pvs.py @@ -0,0 +1,39 @@ +import os +import subprocess + +import pytest + + +@pytest.mark.pvs() +@pytest.mark.skipif( + os.getenv("PVS") == "MOCK", + reason="Skipping real PVS tests in MOCK mode", +) +def test_ping(): + """Test we can ping the PVS device.""" + print("Pinging PVS at", os.environ["PVS"]) + subprocess.check_call(["ping", "-c", "1", os.environ["PVS"]]) + + +@pytest.mark.pvs() +def test_pvs_device_list(sunpowermonitor): + """Test we can get a device list from the PVS.""" + result = sunpowermonitor.device_list() + assert "devices" in result + assert isinstance(result["devices"], list) + assert len(result["devices"]) > 0 + + +@pytest.mark.pvs() +def test_pvs_ess(sunpowermonitor): + """Test we can get an ESS list from the PVS.""" + result = sunpowermonitor.energy_storage_system_status() + assert "errors" in result + assert "ess_report" in result + + +@pytest.mark.pvs() +def test_pvs_network(sunpowermonitor): + """Test we can get an Netork from the PVS.""" + result = sunpowermonitor.network_status() + assert result is not None diff --git a/testing/test_with_sample_data.py b/tests/test_with_sample_data.py similarity index 100% rename from testing/test_with_sample_data.py rename to tests/test_with_sample_data.py From 39f685f8f4434c51a434469ff5c52d675b908dbb Mon Sep 17 00:00:00 2001 From: Keith Baker Date: Wed, 10 Sep 2025 04:53:06 +0000 Subject: [PATCH 07/10] split out hardware ESS tests --- tests/pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/pytest.ini b/tests/pytest.ini index 952eeb4..5df8039 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -2,6 +2,7 @@ makers = pvs: Tests that work directly against a pvs to verify its working correctly (not if PVS env == MOCK this will test the Mock) + ess: Tests that work directly against an ESS to verify its working correctly (not if PVS env == MOCK this will test the Mock) hass: Tests that validate home assistant functionality env = From c6262ae02277d8d0eb9ffd35c9d13774f1a8cba2 Mon Sep 17 00:00:00 2001 From: Keith Baker Date: Wed, 10 Sep 2025 04:53:59 +0000 Subject: [PATCH 08/10] rename to test_hardware log more split out ess tests --- tests/test_hardware.py | 70 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/test_hardware.py diff --git a/tests/test_hardware.py b/tests/test_hardware.py new file mode 100644 index 0000000..e3bd2a6 --- /dev/null +++ b/tests/test_hardware.py @@ -0,0 +1,70 @@ +import logging +import os +import subprocess +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "custom_components")) + +import sunpower # noqa: E402 + +_LOGGER = logging.getLogger(__name__) + + +@pytest.mark.pvs() +@pytest.mark.skipif( + os.getenv("PVS") == "MOCK", + reason="Skipping real PVS tests in MOCK mode", +) +def test_ping(): + """Test we can ping the PVS device.""" + _LOGGER.warning("Pinging PVS at", os.environ["PVS"]) + subprocess.check_call(["ping", "-c", "1", os.environ["PVS"]]) + + +@pytest.mark.pvs() +def test_pvs_device_list(sunpowermonitor): + """Test we can get a device list from the PVS.""" + result = sunpowermonitor.device_list() + _LOGGER.debug("Device List: %s", result) + assert "devices" in result + assert isinstance(result["devices"], list) + assert len(result["devices"]) > 0 + _LOGGER.warning(f"Found {len(result['devices'])} devices in device list") + + +@pytest.mark.ess() +def test_pvs_ess(sunpowermonitor): + """Test we can get an ESS list from the PVS.""" + result = sunpowermonitor.energy_storage_system_status() + _LOGGER.debug("ESS data: %s", result) + assert "errors" in result + assert "ess_report" in result + + +@pytest.mark.pvs() +def test_pvs_network(sunpowermonitor): + """Test we can get an Netork from the PVS.""" + result = sunpowermonitor.network_status() + _LOGGER.debug("Network data: %s", result) + assert result is not None + + +@pytest.mark.pvs() +def test_pvs_parse(sunpowermonitor): + data = sunpower.convert_sunpower_data(sunpowermonitor.device_list()) + _LOGGER.debug("Converted data: %s", data) + assert len(data["PVS"]) > 0 + assert len(data["Inverter"]) > 0 + counts = [(x, len(data[x])) for x in ["PVS", "Inverter", "Power Meter"]] + _LOGGER.warning(f"Found {counts} devices") + + +@pytest.mark.ess() +def test_ess_parse(sunpowermonitor): + data = sunpower.convert_ess_data( + sunpowermonitor.energy_storage_system_status(), + sunpowermonitor.device_list(), + ) + _LOGGER.warning(data) From d856e26fc8fbd4f9a056de48c91d8450e057b302 Mon Sep 17 00:00:00 2001 From: Keith Baker Date: Wed, 10 Sep 2025 04:54:41 +0000 Subject: [PATCH 09/10] missed delete part of rename --- tests/test_pvs.py | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 tests/test_pvs.py diff --git a/tests/test_pvs.py b/tests/test_pvs.py deleted file mode 100644 index 6a42340..0000000 --- a/tests/test_pvs.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -import subprocess - -import pytest - - -@pytest.mark.pvs() -@pytest.mark.skipif( - os.getenv("PVS") == "MOCK", - reason="Skipping real PVS tests in MOCK mode", -) -def test_ping(): - """Test we can ping the PVS device.""" - print("Pinging PVS at", os.environ["PVS"]) - subprocess.check_call(["ping", "-c", "1", os.environ["PVS"]]) - - -@pytest.mark.pvs() -def test_pvs_device_list(sunpowermonitor): - """Test we can get a device list from the PVS.""" - result = sunpowermonitor.device_list() - assert "devices" in result - assert isinstance(result["devices"], list) - assert len(result["devices"]) > 0 - - -@pytest.mark.pvs() -def test_pvs_ess(sunpowermonitor): - """Test we can get an ESS list from the PVS.""" - result = sunpowermonitor.energy_storage_system_status() - assert "errors" in result - assert "ess_report" in result - - -@pytest.mark.pvs() -def test_pvs_network(sunpowermonitor): - """Test we can get an Netork from the PVS.""" - result = sunpowermonitor.network_status() - assert result is not None From f8dfd117998d5433d1ab0f798bd01d5272b3856c Mon Sep 17 00:00:00 2001 From: Keith Baker Date: Wed, 10 Sep 2025 04:59:20 +0000 Subject: [PATCH 10/10] most device tests done, need to fix sample data to be consistent --- tests/test_hardware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_hardware.py b/tests/test_hardware.py index e3bd2a6..fef9070 100644 --- a/tests/test_hardware.py +++ b/tests/test_hardware.py @@ -57,8 +57,8 @@ def test_pvs_parse(sunpowermonitor): _LOGGER.debug("Converted data: %s", data) assert len(data["PVS"]) > 0 assert len(data["Inverter"]) > 0 - counts = [(x, len(data[x])) for x in ["PVS", "Inverter", "Power Meter"]] - _LOGGER.warning(f"Found {counts} devices") + for type, count in [(x, len(data[x])) for x in data.keys()]: + _LOGGER.warning(f"Found {count} devices of type {type}") @pytest.mark.ess()