diff --git a/.coverage b/.coverage index cbd66c9..2c7daa0 100644 Binary files a/.coverage and b/.coverage differ diff --git a/.vscode/settings.json b/.vscode/settings.json index d74721e..1efa477 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,11 @@ "python.analysis.extraPaths": [ "./src/solarlog_cli" ], - "python.analysis.typeCheckingMode": "basic" + "python.analysis.typeCheckingMode": "basic", + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.createEnvironment.contentButton": "show" } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index cff6590..36c31d4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,7 +4,7 @@ { "label": "Pytest", "type": "shell", - "command": "pytest tests", + "command": "pytest tests --snapshot-update", "dependsOn": ["Install all Requirements"], "group": { "kind": "test", diff --git a/src/solarlog_cli/solarlog_client.py b/src/solarlog_cli/solarlog_client.py index f5c4a75..6c426df 100644 --- a/src/solarlog_cli/solarlog_client.py +++ b/src/solarlog_cli/solarlog_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from datetime import datetime +from datetime import datetime, date import json import logging from typing import Any @@ -17,7 +17,7 @@ SolarLogUpdateError, ) -from .solarlog_models import EnergyData, SolarlogData +from .solarlog_models import EnergyData, EventData, SolarlogData SOLARLOG_REQUEST_PAYLOAD = '{ "801": { "170": null } }' _LOGGER = logging.getLogger(__name__) @@ -121,7 +121,7 @@ async def login(self) -> bool: return True - async def execute_http_request(self, body: str, path: str = "getjp", timeout: float | None = None) -> ClientResponse: # pylint: disable=line-too-long + async def execute_http_request(self, body: str, path: str = "getjp", timeout: float | None = None) -> ClientResponse: # pylint: disable=line-too-long """Helper function to process the HTTP Get call.""" if self.session is None: self.session = ClientSession() @@ -214,7 +214,7 @@ async def get_basic_data(self) -> SolarlogData: return data async def get_battery_data(self, timeout: float | None = None) -> list[float]: - """Get battery data from Solar-Log""" + """Get battery data from Solar-Log.""" raw_data: dict = await self.parse_http_response( await self.execute_http_request('{ "858": null }', timeout=timeout) @@ -225,7 +225,7 @@ async def get_battery_data(self, timeout: float | None = None) -> list[float]: return data async def get_power_per_inverter(self, timeout: float | None = None) -> dict[int, float]: - """Get power data from Solar-Log""" + """Get power data from Solar-Log.""" raw_data: dict = await self.parse_http_response( await self.execute_http_request('{ "782": null }', timeout=timeout) @@ -237,7 +237,7 @@ async def get_power_per_inverter(self, timeout: float | None = None) -> dict[int return data async def get_energy_per_inverter(self, timeout: float | None = None) -> dict[int, float]: - """Get power data from Solar-Log""" + """Get power data from Solar-Log.""" raw_data: dict = await self.parse_http_response( await self.execute_http_request('{ "854": null }', timeout=timeout) @@ -253,7 +253,7 @@ async def get_energy_per_inverter(self, timeout: float | None = None) -> dict[in return data async def get_energy(self, timeout: float | None = None) -> EnergyData | None: - """Get energy data from Solar-Log""" + """Get energy data from Solar-Log.""" raw_data: dict = await self.parse_http_response( await self.execute_http_request('{ "878": null }', timeout=timeout) @@ -267,8 +267,21 @@ async def get_energy(self, timeout: float | None = None) -> EnergyData | None: return None - async def get_device_list(self, timeout: float | None = None) -> dict[int, str]: - """Get list of all connected devices.""" + async def get_firmware(self, timeout: float | None = None) -> tuple[str, date]: + """Get firmware data from Solar-Log.""" + + raw_data: dict = await self.parse_http_response( + await self.execute_http_request('{ "801": {"101" : None, "102" : None } }', timeout=timeout) + ) + fw_version: str = raw_data["801"]["101"] + fw_date: date = datetime.strptime( + raw_data["801"]["102"], "%d.%m.%Y").date() + + return (fw_version, fw_date) + + async def get_device_list(self, timeout: float | None = None) -> dict[int, tuple[str, str, str]]: + """Get list of all connected devices. + Return value is a dict with name, possible events and error codes per device.""" # get list of all inverters connected to Solar-Log raw_data: dict = await self.parse_http_response( @@ -276,20 +289,43 @@ async def get_device_list(self, timeout: float | None = None) -> dict[int, str]: ) raw_data = raw_data["740"] - device_list: dict[int, str] = {} + device_list: dict[int, tuple[str, str, str]] = {} for key, value in raw_data.items(): if value != "Err": # get name of the inverter raw_data = await self.parse_http_response( await self.execute_http_request( - f"""{{ "141": {{ "{key}": {{ "119": null }} }} }}""", timeout=timeout + f"""{{ "141": {{ "{key}": {{ "119": null, "708": null, "709": null }} }} }}""", timeout=timeout ) ) - device_list |= {int(key): raw_data["141"][key]["119"]} + device_list |= {int(key): ( + raw_data["141"][key]["119"], raw_data["141"][key]["708"], raw_data["141"][key]["709"])} return device_list + async def get_device_last_event(self, device: int, timeout: float | None = None) -> EventData: + """Get list of last event/error of a device.""" + + raw_data = await self.parse_http_response( + await self.execute_http_request( + f"""{{ "141": {{ "{device}": {{ "710": null }} }} }}""", timeout=timeout + ) + ) + print(raw_data) + events = raw_data["141"][str(device)]["710"]["0"] + + return EventData(start=datetime.fromtimestamp(events[0][0]), end=datetime.fromtimestamp(events[0][1]), event=int(events[0][3]), error=int(events[0][4])) + + async def get_status_per_device(self, timeout: float | None = None) -> dict[int, str]: + """Get inverter status from Solar-Log""" + + raw_data: dict = await self.parse_http_response( + await self.execute_http_request('{ "608": null }', timeout=timeout) + ) + + return raw_data["608"] + async def close(self) -> None: """Close open client session.""" if self.session and self._close_session: diff --git a/src/solarlog_cli/solarlog_connector.py b/src/solarlog_cli/solarlog_connector.py index 10dbd5a..d663795 100644 --- a/src/solarlog_cli/solarlog_connector.py +++ b/src/solarlog_cli/solarlog_connector.py @@ -1,6 +1,6 @@ """Connector class to manage access to Solar-Log.""" -from datetime import timezone, tzinfo +from datetime import date, timezone, tzinfo import logging from zoneinfo import ZoneInfo @@ -164,13 +164,18 @@ async def update_device_list(self, timeout: float | None = None) -> dict[int, In devices = await self.client.get_device_list(timeout) self._device_list = { - key: InverterData(name=value,enabled=self.device(key).enabled) + key: InverterData(name=value[0],enabled=self.device(key).enabled) for key, value in devices.items() } _LOGGER.debug("Device list: %s",self._device_list) return self._device_list + async def update_firmware_information(self, timeout: float | None = None) -> tuple[str, date]: + """Update firmware data (version and release date).""" + + return await self.client.get_firmware(timeout) + async def update_inverter_data(self, timeout: float | None = None) -> dict[int, InverterData]: """Update device specific data.""" @@ -185,6 +190,14 @@ async def update_inverter_data(self, timeout: float | None = None) -> dict[int, if self._device_list.get(key,InverterData).enabled: self._device_list[key].consumption_year = float(value) + raw_data = await self.client.get_status_per_device(timeout) + for key, value in raw_data.items(): + key = int(key) + if self._device_list.get(key,InverterData).enabled: + self._device_list[key].status = value + + self._device_list[key].last_event = await self.client.get_device_last_event(key) + _LOGGER.debug("Inverter data updated: %s",self._device_list) return self._device_list diff --git a/src/solarlog_cli/solarlog_models.py b/src/solarlog_cli/solarlog_models.py index 1340350..cf37904 100644 --- a/src/solarlog_cli/solarlog_models.py +++ b/src/solarlog_cli/solarlog_models.py @@ -1,6 +1,6 @@ """Models for SolarLog.""" from dataclasses import dataclass, field -from datetime import datetime +from datetime import date, datetime from mashumaro import DataClassDictMixin @@ -20,14 +20,27 @@ class EnergyData(): production: float | None = None self_consumption: float | None = None +@dataclass +class EventData(): + """Event Data model.""" + + end: datetime + error: int + event: int + start: datetime + @dataclass class InverterData(): """Inverter Data model.""" name: str = "" - enabled: bool = False - current_power: float | None = None consumption_year: float | None = None + current_power: float | None = None + enabled: bool = False + errors: dict[int,str] = field(default_factory=dict) + events: dict[int,str] = field(default_factory=dict) + last_event: EventData | None = None + status: str | None = None @dataclass @@ -63,6 +76,8 @@ class SolarlogData(DataClassDictMixin): #extended data battery_data: BatteryData | None = None + firmware_date: date | None = None + firmware_version: int | None = None inverter_data: dict[int, InverterData] = field(default_factory=dict) production_year: float | None = None self_consumption_year: float | None = None diff --git a/tests/__snapshots__/test_solarlog_cli.ambr b/tests/__snapshots__/test_solarlog_cli.ambr index 79b3d13..2e4dfba 100644 --- a/tests/__snapshots__/test_solarlog_cli.ambr +++ b/tests/__snapshots__/test_solarlog_cli.ambr @@ -21,25 +21,64 @@ 'consumption_year': 4227027.0, 'current_power': 3170.0, 'enabled': True, + 'errors': dict({ + }), + 'events': dict({ + }), + 'last_event': dict({ + 'end': datetime.datetime(2026, 1, 12, 17, 33, 59), + 'error': 0, + 'event': 6, + 'start': datetime.datetime(2026, 1, 12, 8, 13, 31), + }), 'name': '', + 'status': 'Power', }), 1: dict({ 'consumption_year': 1920650.0, 'current_power': None, 'enabled': True, + 'errors': dict({ + }), + 'events': dict({ + }), + 'last_event': dict({ + 'end': datetime.datetime(2026, 1, 12, 17, 33, 59), + 'error': 0, + 'event': 6, + 'start': datetime.datetime(2026, 1, 12, 8, 13, 31), + }), 'name': '', + 'status': 'Power', }), 2: dict({ 'consumption_year': None, 'current_power': None, 'enabled': False, + 'errors': dict({ + }), + 'events': dict({ + }), + 'last_event': None, 'name': '', + 'status': None, }), 3: dict({ 'consumption_year': None, 'current_power': 2816.0, 'enabled': True, + 'errors': dict({ + }), + 'events': dict({ + }), + 'last_event': dict({ + 'end': datetime.datetime(2026, 1, 12, 17, 33, 59), + 'error': 0, + 'event': 6, + 'start': datetime.datetime(2026, 1, 12, 8, 13, 31), + }), 'name': '', + 'status': 'OFF', }), }), 'last_updated': datetime.datetime(2024, 8, 26, 14, 19, 45, tzinfo=zoneinfo.ZoneInfo(key='UTC')), @@ -65,25 +104,49 @@ 'consumption_year': None, 'current_power': None, 'enabled': True, + 'errors': dict({ + }), + 'events': dict({ + }), + 'last_event': None, 'name': 'Device 1', + 'status': None, }), 1: dict({ 'consumption_year': None, 'current_power': None, 'enabled': False, + 'errors': dict({ + }), + 'events': dict({ + }), + 'last_event': None, 'name': 'Device 2', + 'status': None, }), 2: dict({ 'consumption_year': None, 'current_power': None, 'enabled': False, + 'errors': dict({ + }), + 'events': dict({ + }), + 'last_event': None, 'name': 'Device 3', + 'status': None, }), 3: dict({ 'consumption_year': None, 'current_power': None, 'enabled': True, + 'errors': dict({ + }), + 'events': dict({ + }), + 'last_event': None, 'name': 'Device 4', + 'status': None, }), }) # --- diff --git a/tests/fixtures/device_data_1.json b/tests/fixtures/device_data_1.json index b913068..65d1da0 100644 --- a/tests/fixtures/device_data_1.json +++ b/tests/fixtures/device_data_1.json @@ -1,7 +1,19 @@ { "141": { "0": { - "119":"Device 1" + "119":"Device 1", + "708":{ + "0":"No Current", + "1":" Power", + "2":"", + "3":"" + }, + "709":{ + "0":" ", + "1":"No Current", + "2":"Power", + "3":"" + } } } } \ No newline at end of file diff --git a/tests/fixtures/device_data_2.json b/tests/fixtures/device_data_2.json index 7e9e683..82166c6 100644 --- a/tests/fixtures/device_data_2.json +++ b/tests/fixtures/device_data_2.json @@ -1,7 +1,19 @@ { "141": { "1": { - "119":"Device 2" + "119":"Device 2", + "708":{ + "0":"No Current", + "1":" Power", + "2":"", + "3":"" + }, + "709":{ + "0":" ", + "1":"No Current", + "2":"Power", + "3":"" + } } } } \ No newline at end of file diff --git a/tests/fixtures/device_data_3.json b/tests/fixtures/device_data_3.json index 8dfc488..2bc8540 100644 --- a/tests/fixtures/device_data_3.json +++ b/tests/fixtures/device_data_3.json @@ -1,7 +1,19 @@ { "141": { "2": { - "119":"Device 3" + "119":"Device 3", + "708":{ + "0":"No Current", + "1":" Power", + "2":"", + "3":"" + }, + "709":{ + "0":" ", + "1":"No Current", + "2":"Power", + "3":"" + } } } } \ No newline at end of file diff --git a/tests/fixtures/device_data_4.json b/tests/fixtures/device_data_4.json index 29d621a..492c66e 100644 --- a/tests/fixtures/device_data_4.json +++ b/tests/fixtures/device_data_4.json @@ -1,7 +1,19 @@ { "141": { "3": { - "119":"Device 4" + "119":"Device 4", + "708":{ + "0":"No Current", + "1":" Power", + "2":"", + "3":"" + }, + "709":{ + "0":" ", + "1":"No Current", + "2":"Power", + "3":"" + } } } } \ No newline at end of file diff --git a/tests/fixtures/device_error_list.json b/tests/fixtures/device_error_list.json new file mode 100644 index 0000000..43bbeaa --- /dev/null +++ b/tests/fixtures/device_error_list.json @@ -0,0 +1,11 @@ +{ + "141": { + "3":{ + "708":{ + "0":" ", + "1":"Undefined", + "2":"" + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/device_event_list_1.json b/tests/fixtures/device_event_list_1.json new file mode 100644 index 0000000..4a746fe --- /dev/null +++ b/tests/fixtures/device_event_list_1.json @@ -0,0 +1,12 @@ +{ + "141": { + "0": { + "710": { + "0": [ + [1768205611,1768239239,3,6,0], + [1768239240,1768292129,3,0,0] + ] + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/device_event_list_2.json b/tests/fixtures/device_event_list_2.json new file mode 100644 index 0000000..2494aff --- /dev/null +++ b/tests/fixtures/device_event_list_2.json @@ -0,0 +1,12 @@ +{ + "141": { + "1": { + "710": { + "0": [ + [1768205611,1768239239,3,6,0], + [1768239240,1768292129,3,0,0] + ] + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/device_event_list_3.json b/tests/fixtures/device_event_list_3.json new file mode 100644 index 0000000..afe94c2 --- /dev/null +++ b/tests/fixtures/device_event_list_3.json @@ -0,0 +1,12 @@ +{ + "141": { + "2": { + "710": { + "0": [ + [1768205611,1768239239,3,6,0], + [1768239240,1768292129,3,0,0] + ] + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/device_event_list_4.json b/tests/fixtures/device_event_list_4.json new file mode 100644 index 0000000..61a11f4 --- /dev/null +++ b/tests/fixtures/device_event_list_4.json @@ -0,0 +1,12 @@ +{ + "141": { + "3": { + "710": { + "0": [ + [1768205611,1768239239,3,6,0], + [1768239240,1768292129,3,0,0] + ] + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/device_last_event.json b/tests/fixtures/device_last_event.json new file mode 100644 index 0000000..4dde43a --- /dev/null +++ b/tests/fixtures/device_last_event.json @@ -0,0 +1,27 @@ +{ + "141": { + "3":{ + "708":{ + "0":"OFF", + "1":"INIT", + "2":"IsoMeas", + "3":"GridCheck", + "4":"StartUp","5":" ", + "6":"FeedIn", + "7":"Throttled", + "8":"ExtSwitchOff", + "9":"Update", + "10":"Standby", + "11":"GridSync", + "12":"GridPreCheck", + "13":"GridSwitchOff", + "14":"Overheating", + "15":"Shutdown", + "16":"ImproperDcVoltage", + "17":"ESB", + "18":"Undefined", + "19":"" + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/device_status.json b/tests/fixtures/device_status.json new file mode 100644 index 0000000..578696a --- /dev/null +++ b/tests/fixtures/device_status.json @@ -0,0 +1,9 @@ +{ + "608":{ + "0":"Power", + "1":"Power", + "2":"Off", + "3":"OFF", + "4":"OFFLINE" + } +} \ No newline at end of file diff --git a/tests/fixtures/firmware_data.json b/tests/fixtures/firmware_data.json new file mode 100644 index 0000000..10e44f5 --- /dev/null +++ b/tests/fixtures/firmware_data.json @@ -0,0 +1,6 @@ +{ + "801": { + "101":16974156, + "102":"16.07.2024" + } +} diff --git a/tests/test_solarlog_cli.py b/tests/test_solarlog_cli.py index 85c4639..df2138f 100644 --- a/tests/test_solarlog_cli.py +++ b/tests/test_solarlog_cli.py @@ -238,6 +238,22 @@ async def test_update_data( "http://solarlog.com/getjp", body=load_fixture("energy_per_inverter.json"), ) + responses.post( + "http://solarlog.com/getjp", + body=load_fixture("device_status.json"), + ) + responses.post( + "http://solarlog.com/getjp", + body=load_fixture("device_event_list_1.json"), + ) + responses.post( + "http://solarlog.com/getjp", + body=load_fixture("device_event_list_2.json"), + ) + responses.post( + "http://solarlog.com/getjp", + body=load_fixture("device_event_list_4.json"), + ) responses.post( "http://solarlog.com/getjp", body=load_fixture("battery_data.json"), @@ -278,10 +294,30 @@ async def test_update_data_without_battery( "http://solarlog.com/getjp", body=load_fixture("energy_per_inverter.json"), ) + responses.post( + "http://solarlog.com/getjp", + body=load_fixture("device_status.json"), + ) + responses.post( + "http://solarlog.com/getjp", + body=load_fixture("device_event_list_1.json"), + ) + responses.post( + "http://solarlog.com/getjp", + body=load_fixture("device_event_list_2.json"), + ) + responses.post( + "http://solarlog.com/getjp", + body=load_fixture("device_event_list_4.json"), + ) responses.post( "http://solarlog.com/getjp", body=load_fixture("battery_data_without_battery.json"), ) + responses.post( + "http://solarlog.com/getjp", + body=load_fixture("firmware_data.json"), + ) solarlog_connector = SolarLogConnector( "http://solarlog.com", @@ -294,6 +330,10 @@ async def test_update_data_without_battery( assert data.battery_data is None + firmware = await solarlog_connector.update_firmware_information() + + assert firmware[0] == 16974156 + await solarlog_connector.client.close() assert solarlog_connector.client.session.closed