solarlog-cli 0.7.1__tar.gz → 0.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.devcontainer/devcontainer.json +3 -3
  2. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.github/workflows/python-publish.yml +1 -1
  3. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.github/workflows/tests.yml +1 -1
  4. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.gitignore +1 -0
  5. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/PKG-INFO +3 -3
  6. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/pyproject.toml +2 -2
  7. solarlog_cli-0.7.2/pytest.ini +3 -0
  8. solarlog_cli-0.7.2/requirements.txt +6 -0
  9. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_client.py +85 -32
  10. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_connector.py +37 -14
  11. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_models.py +23 -6
  12. solarlog_cli-0.7.2/tests/__snapshots__/test_solarlog_cli.ambr +180 -0
  13. solarlog_cli-0.7.2/tests/conftest.py +29 -0
  14. solarlog_cli-0.7.2/tests/fixtures/device_data_1.json +16 -0
  15. solarlog_cli-0.7.2/tests/fixtures/device_data_2.json +16 -0
  16. solarlog_cli-0.7.2/tests/fixtures/device_data_3.json +16 -0
  17. solarlog_cli-0.7.2/tests/fixtures/device_data_4.json +16 -0
  18. solarlog_cli-0.7.2/tests/fixtures/device_error_list.json +11 -0
  19. solarlog_cli-0.7.2/tests/fixtures/device_event_list_1.json +12 -0
  20. solarlog_cli-0.7.2/tests/fixtures/device_event_list_2.json +12 -0
  21. solarlog_cli-0.7.2/tests/fixtures/device_event_list_3.json +12 -0
  22. solarlog_cli-0.7.2/tests/fixtures/device_event_list_4.json +12 -0
  23. solarlog_cli-0.7.2/tests/fixtures/device_last_event.json +27 -0
  24. solarlog_cli-0.7.2/tests/fixtures/device_status.json +9 -0
  25. solarlog_cli-0.7.2/tests/fixtures/firmware_data.json +6 -0
  26. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/ruff.toml +1 -1
  27. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/syrupy.py +1 -2
  28. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/test_solarlog_cli.py +115 -52
  29. solarlog_cli-0.7.1/.coverage +0 -0
  30. solarlog_cli-0.7.1/pytest.ini +0 -2
  31. solarlog_cli-0.7.1/requirements.txt +0 -7
  32. solarlog_cli-0.7.1/tests/__snapshots__/test_solarlog_cli.ambr +0 -95
  33. solarlog_cli-0.7.1/tests/conftest.py +0 -22
  34. solarlog_cli-0.7.1/tests/fixtures/device_data_1.json +0 -7
  35. solarlog_cli-0.7.1/tests/fixtures/device_data_2.json +0 -7
  36. solarlog_cli-0.7.1/tests/fixtures/device_data_3.json +0 -7
  37. solarlog_cli-0.7.1/tests/fixtures/device_data_4.json +0 -7
  38. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.github/workflows/pylint.yml +0 -0
  39. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.github/workflows/ruff.yml +0 -0
  40. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.github/workflows/typing.yml +0 -0
  41. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.vscode/settings.json +0 -0
  42. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.vscode/tasks.json +0 -0
  43. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/LICENSE +0 -0
  44. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/README.md +0 -0
  45. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/src/solarlog_cli/__init__.py +0 -0
  46. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/src/solarlog_cli/py.typed +0 -0
  47. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_exceptions.py +0 -0
  48. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/__init__.py +0 -0
  49. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/basic_data.json +0 -0
  50. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/basic_data_during_update.json +0 -0
  51. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/basic_data_no_power.json +0 -0
  52. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/battery_data.json +0 -0
  53. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/battery_data_without_battery.json +0 -0
  54. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/device_list.json +0 -0
  55. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/device_list_access_denied.json +0 -0
  56. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/energy_per_inverter.json +0 -0
  57. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/extended_data.json +0 -0
  58. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/power_per_inverter.json +0 -0
  59. {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/user_salts.json +0 -0
@@ -3,10 +3,10 @@
3
3
  {
4
4
  "name": "Docker in Docker",
5
5
  // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6
- "image": "mcr.microsoft.com/devcontainers/base:bullseye",
6
+ "image": "mcr.microsoft.com/devcontainers/python:3-3.14-bookworm",
7
7
 
8
8
  "features": {
9
- "ghcr.io/devcontainers/features/docker-in-docker:2": {
9
+ "ghcr.io/devcontainers/features/docker-in-docker:4.1.3": {
10
10
  "moby": true,
11
11
  "azureDnsAutoDetection": true,
12
12
  "installDockerBuildx": true,
@@ -24,7 +24,7 @@
24
24
  "ghcr.io/devcontainers-community/npm-features/prettier:1": {
25
25
  "version": "latest"
26
26
  },
27
- "ghcr.io/hspaans/devcontainer-features/pytest:1": {
27
+ "ghcr.io/hspaans/devcontainer-features/pytest:2.0.0": {
28
28
  "version": "latest",
29
29
  "plugins": "pytest-testinfra"
30
30
  }
@@ -33,7 +33,7 @@ jobs:
33
33
  - name: Build package
34
34
  run: python -m build
35
35
  - name: Publish package
36
- uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
36
+ uses: pypa/gh-action-pypi-publish@release/v1
37
37
  with:
38
38
  user: __token__
39
39
  password: ${{ secrets.PYPI_API_TOKEN }}
@@ -30,7 +30,7 @@ jobs:
30
30
  - name: 🏗 Install dependencies
31
31
  run: python3 -m pip install -r requirements.txt
32
32
  - name: 🏗 Install pytest
33
- run: python3 -m pip install pytest pytest-cov pytest-aio
33
+ run: python3 -m pip install pytest pytest-cov pytest-asyncio
34
34
  - name: 🚀 Run pytest
35
35
  run: pytest --cov src tests
36
36
  - name: ⬆️ Upload coverage artifact
@@ -1,5 +1,6 @@
1
1
  src/solarlog_cli/__pycache__
2
2
  tests/__pycache__
3
+ .coverage
3
4
  .mypy_cache
4
5
  .pytest_cache
5
6
  .gitignore
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: solarlog_cli
3
- Version: 0.7.1
3
+ Version: 0.7.2
4
4
  Summary: Python library to access the Solar-Log
5
5
  Project-URL: Repository, https://github.com/dontinelli/solarlog_cli.git
6
6
  Project-URL: Issues, https://github.com/dontinelli/solarlog_cli/issues
@@ -14,7 +14,7 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Requires-Python: >=3.12
17
- Requires-Dist: aiohttp
17
+ Requires-Dist: aiohttp>=3.14.0
18
18
  Requires-Dist: bcrypt>=4.3.0
19
19
  Requires-Dist: mashumaro>=3.13
20
20
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "solarlog_cli"
3
- version = "0.7.1"
3
+ version = "0.7.2"
4
4
  authors = [
5
5
  { name="dontinelli", email="73341522+dontinelli@users.noreply.github.com" },
6
6
  ]
@@ -8,7 +8,7 @@ description = "Python library to access the Solar-Log"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.12"
10
10
  dependencies = [
11
- "aiohttp",
11
+ "aiohttp>=3.14.0",
12
12
  "bcrypt>=4.3.0",
13
13
  "mashumaro>=3.13",
14
14
  ]
@@ -0,0 +1,3 @@
1
+ [pytest]
2
+ pythonpath = src
3
+ asyncio_mode = auto
@@ -0,0 +1,6 @@
1
+ aiohttp>=3.14.0
2
+ aiointercept[pytest-asyncio] == 1.0.0
3
+ bcrypt >= 4.3.0
4
+ mashumaro>=3.13
5
+ pytest-cov==5.0.0
6
+ syrupy == 4.6.1
@@ -2,22 +2,21 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import asyncio
6
- from datetime import datetime
7
5
  import json
8
6
  import logging
7
+ from datetime import UTC, date, datetime, tzinfo
9
8
  from typing import Any
9
+ from zoneinfo import ZoneInfo
10
10
 
11
- from aiohttp import ClientResponse, ClientSession, ClientTimeout
12
11
  import bcrypt
12
+ from aiohttp import ClientResponse, ClientSession, ClientTimeout
13
13
 
14
14
  from .solarlog_exceptions import (
15
15
  SolarLogAuthenticationError,
16
16
  SolarLogConnectionError,
17
17
  SolarLogUpdateError,
18
18
  )
19
-
20
- from .solarlog_models import EnergyData, SolarlogData
19
+ from .solarlog_models import EnergyData, EventData, SolarlogData
21
20
 
22
21
  SOLARLOG_REQUEST_PAYLOAD = '{ "801": { "170": null } }'
23
22
  _LOGGER = logging.getLogger(__name__)
@@ -27,10 +26,15 @@ class Client:
27
26
  """Client class to access Solar-Log."""
28
27
 
29
28
  # pylint: disable=too-many-positional-arguments
29
+ # pylint: disable=too-many-instance-attributes
30
+
31
+ def __init__(
32
+ self, host: str, session: ClientSession | None, password: str = "", tz: str = ""
33
+ ) -> None:
30
34
 
31
- def __init__(self, host: str, session: ClientSession | None, password: str = "") -> None:
32
35
  self.host: str = host
33
36
  self.password: str = password
37
+ self.timezone: tzinfo = UTC if tz == "" else ZoneInfo(tz)
34
38
 
35
39
  self.request_timeout = 30
36
40
 
@@ -41,7 +45,7 @@ class Client:
41
45
 
42
46
  self._hashed_pwd: bool = False
43
47
  self._close_session: bool = True
44
- self._token = "" #required for older firmware, where cookie is not sufficient
48
+ self._token = "" # required for older firmware, where cookie is not sufficient
45
49
 
46
50
  async def test_connection(self) -> bool:
47
51
  """Test the connection to Solar-Log."""
@@ -56,13 +60,10 @@ class Client:
56
60
  timeout=ClientTimeout(total=self.request_timeout)
57
61
  )
58
62
 
59
- if response.status == 200:
60
- return True
61
-
62
- return False
63
+ return response.status == 200
63
64
 
64
65
  async def login(self) -> bool:
65
- """Test the connection to Solar-Log."""
66
+ """Login to Solar-Log."""
66
67
 
67
68
  if self.password == "":
68
69
  return False
@@ -72,7 +73,7 @@ class Client:
72
73
  response = await self.execute_http_request(payload, "login")
73
74
 
74
75
  text = await response.text()
75
- _LOGGER.debug("Response: %s", text)
76
+ _LOGGER.debug("Response to request for login: %s", text)
76
77
  if text.count("FAILED - User was wrong"):
77
78
  # Response means, that no password is required
78
79
  self.password = ""
@@ -125,7 +126,7 @@ class Client:
125
126
 
126
127
  return True
127
128
 
128
- async def execute_http_request(self, body: str, path: str = "getjp", timeout: float | None = None) -> ClientResponse: # pylint: disable=line-too-long
129
+ async def execute_http_request(self, body: str, path: str = "getjp", timeout: float | None = None) -> ClientResponse: # pylint: disable=line-too-long
129
130
  """Helper function to process the HTTP Get call."""
130
131
  if self.session is None:
131
132
  self.session = ClientSession()
@@ -138,8 +139,8 @@ class Client:
138
139
  if self._token != "":
139
140
  body = f"token={self._token}; " + body
140
141
 
141
- _LOGGER.debug("HTTP-request header: %s", header)
142
- _LOGGER.debug("HTTP-request body: %s", body)
142
+ # _LOGGER.debug("HTTP-request header: %s", header)
143
+ # _LOGGER.debug("HTTP-request body: %s", body)
143
144
 
144
145
  try:
145
146
  response = await self.session.post(
@@ -148,7 +149,7 @@ class Client:
148
149
  data=body,
149
150
  timeout=ClientTimeout(total=timeout or self.request_timeout),
150
151
  )
151
- except asyncio.TimeoutError as exception:
152
+ except TimeoutError as exception:
152
153
  msg = f"Timeout occurred while connecting to Solar-Log at {self.host}"
153
154
  raise SolarLogConnectionError(msg) from exception
154
155
 
@@ -163,14 +164,14 @@ class Client:
163
164
  {"Content-Type": content_type, "response": text},
164
165
  )
165
166
 
166
- _LOGGER.debug("HTTP-request successful: %s", response)
167
+ # _LOGGER.debug("HTTP-request successful: %s", response)
167
168
  return response
168
169
 
169
170
  async def parse_http_response(self, response: ClientResponse) -> dict[str, Any]:
170
171
  """Helper function to parse the HTTP response."""
171
172
 
172
173
  text = await response.text(errors="replace")
173
- _LOGGER.debug("Parsing http response: %s", text)
174
+ # _LOGGER.debug("Parsing http response: %s", text)
174
175
 
175
176
  if text.count('{"QUERY IMPOSSIBLE 000"}'):
176
177
  raise SolarLogUpdateError(f"Server response: {text}")
@@ -199,7 +200,7 @@ class Client:
199
200
 
200
201
  data = SolarlogData(
201
202
  last_updated=datetime.strptime(
202
- raw_data["100"], "%d.%m.%y %H:%M:%S"),
203
+ raw_data["100"], "%d.%m.%y %H:%M:%S").astimezone(self.timezone),
203
204
  power_ac=raw_data["101"],
204
205
  power_dc=raw_data["102"],
205
206
  voltage_ac=raw_data["103"],
@@ -221,7 +222,7 @@ class Client:
221
222
  return data
222
223
 
223
224
  async def get_battery_data(self, timeout: float | None = None) -> list[float]:
224
- """Get battery data from Solar-Log"""
225
+ """Get battery data from Solar-Log."""
225
226
 
226
227
  raw_data: dict = await self.parse_http_response(
227
228
  await self.execute_http_request('{ "858": null }', timeout=timeout)
@@ -232,7 +233,7 @@ class Client:
232
233
  return data
233
234
 
234
235
  async def get_power_per_inverter(self, timeout: float | None = None) -> dict[int, float]:
235
- """Get power data from Solar-Log"""
236
+ """Get power data from Solar-Log."""
236
237
 
237
238
  raw_data: dict = await self.parse_http_response(
238
239
  await self.execute_http_request('{ "782": null }', timeout=timeout)
@@ -244,7 +245,7 @@ class Client:
244
245
  return data
245
246
 
246
247
  async def get_energy_per_inverter(self, timeout: float | None = None) -> dict[int, float]:
247
- """Get power data from Solar-Log"""
248
+ """Get power data from Solar-Log."""
248
249
 
249
250
  raw_data: dict = await self.parse_http_response(
250
251
  await self.execute_http_request('{ "854": null }', timeout=timeout)
@@ -260,7 +261,7 @@ class Client:
260
261
  return data
261
262
 
262
263
  async def get_energy(self, timeout: float | None = None) -> EnergyData | None:
263
- """Get energy data from Solar-Log"""
264
+ """Get energy data from Solar-Log."""
264
265
 
265
266
  raw_data: dict = await self.parse_http_response(
266
267
  await self.execute_http_request('{ "878": null }', timeout=timeout)
@@ -274,8 +275,26 @@ class Client:
274
275
 
275
276
  return None
276
277
 
277
- async def get_device_list(self, timeout: float | None = None) -> dict[int, str]:
278
- """Get list of all connected devices."""
278
+ async def get_firmware(self, timeout: float | None = None) -> tuple[str, date]:
279
+ """Get firmware data from Solar-Log."""
280
+
281
+ raw_data: dict = await self.parse_http_response(
282
+ await self.execute_http_request(
283
+ '{ "801": {"101" : None, "102" : None } }', timeout=timeout
284
+ )
285
+ )
286
+ fw_version: str = raw_data["801"]["101"]
287
+ fw_date: date = datetime.strptime(
288
+ raw_data["801"]["102"], "%d.%m.%Y").date()
289
+
290
+ return (fw_version, fw_date)
291
+
292
+ async def get_device_list(
293
+ self,
294
+ timeout: float | None = None
295
+ ) -> dict[int, tuple[str, dict, dict]]:
296
+ """Get list of all connected devices.
297
+ Return value is a dict with name, possible events and error codes per device."""
279
298
 
280
299
  # get list of all inverters connected to Solar-Log
281
300
  raw_data: dict = await self.parse_http_response(
@@ -283,20 +302,54 @@ class Client:
283
302
  )
284
303
  raw_data = raw_data["740"]
285
304
 
286
- device_list: dict[int, str] = {}
305
+ device_list: dict[int, tuple[str, dict, dict]] = {}
287
306
 
288
307
  for key, value in raw_data.items():
289
308
  if value != "Err":
290
- # get name of the inverter
309
+ # get name, events and error codes of the inverter
310
+ query = (
311
+ f'{{ "141": {{ "{key}": {{ "119": null, "708": null, '
312
+ f'"709": null }} }} }}'
313
+ )
291
314
  raw_data = await self.parse_http_response(
292
- await self.execute_http_request(
293
- f"""{{ "141": {{ "{key}": {{ "119": null }} }} }}""", timeout=timeout
294
- )
315
+ await self.execute_http_request(query, timeout=timeout)
295
316
  )
296
- device_list |= {int(key): raw_data["141"][key]["119"]}
317
+ device_list |= {int(key): (
318
+ raw_data["141"][key]["119"],
319
+ raw_data["141"][key]["708"],
320
+ raw_data["141"][key]["709"],
321
+ )
322
+ }
297
323
 
298
324
  return device_list
299
325
 
326
+ async def get_device_last_event(self, device: int, timeout: float | None = None) -> EventData:
327
+ """Get list of last event/error of a device."""
328
+
329
+ raw_data = await self.parse_http_response(
330
+ await self.execute_http_request(
331
+ f"""{{ "141": {{ "{device}": {{ "710": null }} }} }}""", timeout=timeout
332
+ )
333
+ )
334
+ print(raw_data)
335
+ events = raw_data["141"][str(device)]["710"]["0"]
336
+
337
+ return EventData(
338
+ start=datetime.fromtimestamp(events[0][0]),
339
+ end=datetime.fromtimestamp(events[0][1]),
340
+ event=int(events[0][3]),
341
+ error=int(events[0][4])
342
+ )
343
+
344
+ async def get_status_per_device(self, timeout: float | None = None) -> dict[int, str]:
345
+ """Get inverter status from Solar-Log"""
346
+
347
+ raw_data: dict = await self.parse_http_response(
348
+ await self.execute_http_request('{ "608": null }', timeout=timeout)
349
+ )
350
+ print(raw_data)
351
+ return raw_data["608"]
352
+
300
353
  async def close(self) -> None:
301
354
  """Close open client session."""
302
355
  if self.session and self._close_session:
@@ -1,13 +1,12 @@
1
1
  """Connector class to manage access to Solar-Log."""
2
2
 
3
- from datetime import timezone, tzinfo
4
3
  import logging
5
- from zoneinfo import ZoneInfo
4
+ from datetime import date
6
5
 
7
6
  from aiohttp import ClientSession
8
7
 
9
8
  from .solarlog_client import Client
10
- from .solarlog_exceptions import(
9
+ from .solarlog_exceptions import (
11
10
  SolarLogAuthenticationError,
12
11
  SolarLogConnectionError,
13
12
  SolarLogUpdateError,
@@ -26,13 +25,15 @@ class SolarLogConnector:
26
25
  def __init__(
27
26
  self,
28
27
  host: str,
28
+ auto_fw_update: bool = False,
29
29
  extended_data: bool = False,
30
30
  tz: str = "",
31
31
  device_enabled: dict[int, bool] | None = None,
32
32
  password: str = "",
33
33
  session: ClientSession | None = None,
34
34
  ):
35
- self.client = Client(host, session, password)
35
+ self.client = Client(host, session, password, tz)
36
+ self.auto_fw_update: bool = auto_fw_update
36
37
  self.extended_data: bool = extended_data
37
38
 
38
39
  self._device_list: dict[int, InverterData] = {}
@@ -42,7 +43,6 @@ class SolarLogConnector:
42
43
  for key, value in device_enabled.items():
43
44
  self._device_list |= {key: InverterData(enabled=value)}
44
45
 
45
- self.timezone: tzinfo = timezone.utc if tz == "" else ZoneInfo(tz)
46
46
 
47
47
  async def test_connection(self) -> bool:
48
48
  """Test if connection to Solar-Log works."""
@@ -98,6 +98,12 @@ class SolarLogConnector:
98
98
 
99
99
  data.battery_data = await self.update_battery_data()
100
100
 
101
+ if self.auto_fw_update:
102
+ (
103
+ data.firmware_version,
104
+ data.firmware_release_date,
105
+ ) = await self.update_firmware_information()
106
+
101
107
  _LOGGER.debug("Extended data updated: %s",data)
102
108
 
103
109
  return data
@@ -112,8 +118,6 @@ class SolarLogConnector:
112
118
  "Invalid data returned (can happen after Solarlog restart)."
113
119
  )
114
120
 
115
- data.last_updated = data.last_updated.replace(tzinfo=self.timezone)
116
-
117
121
  _LOGGER.debug("Basic data updated: %s",data)
118
122
 
119
123
  #calculated values (for downward compatibility)
@@ -124,7 +128,7 @@ class SolarLogConnector:
124
128
  data.usage = data.consumption_ac / data.power_ac * 100
125
129
  data.power_available = data.power_ac - data.consumption_ac
126
130
  else:
127
- data.usage = 0.0
131
+ data.usage = None
128
132
  data.power_available = 0.0
129
133
  if data.total_power != 0:
130
134
  data.capacity = data.power_dc / data.total_power * 100
@@ -164,27 +168,46 @@ class SolarLogConnector:
164
168
  devices = await self.client.get_device_list(timeout)
165
169
 
166
170
  self._device_list = {
167
- key: InverterData(name=value,enabled=self.device(key).enabled)
171
+ key: InverterData(
172
+ name=value[0],
173
+ enabled=self.device(key).enabled,
174
+ errors=value[2],
175
+ events=value[1]
176
+ )
168
177
  for key, value in devices.items()
169
178
  }
170
- _LOGGER.debug("Device list: %s",self._device_list)
179
+ _LOGGER.debug("Device list updated: %s",self._device_list)
171
180
 
172
181
  return self._device_list
173
182
 
183
+ async def update_firmware_information(self, timeout: float | None = None) -> tuple[str, date]:
184
+ """Update firmware data (version and release date)."""
185
+
186
+ return await self.client.get_firmware(timeout)
187
+
174
188
  async def update_inverter_data(self, timeout: float | None = None) -> dict[int, InverterData]:
175
189
  """Update device specific data."""
176
190
 
177
- raw_data = await self.client.get_power_per_inverter(timeout)
178
- for key, value in raw_data.items():
191
+ raw_power_data = await self.client.get_power_per_inverter(timeout)
192
+ for key, value in raw_power_data.items():
179
193
  key = int(key)
180
194
  if self._device_list.get(key,InverterData).enabled:
181
195
  self._device_list[key].current_power = float(value)
182
196
 
183
- raw_data = await self.client.get_energy_per_inverter(timeout)
184
- for key, value in raw_data.items():
197
+ raw_energy_data = await self.client.get_energy_per_inverter(timeout)
198
+ for key, value in raw_energy_data.items():
199
+ key = int(key)
185
200
  if self._device_list.get(key,InverterData).enabled:
186
201
  self._device_list[key].consumption_year = float(value)
187
202
 
203
+ raw_status_data = await self.client.get_status_per_device(timeout)
204
+ for key, str_value in raw_status_data.items():
205
+ key = int(key)
206
+ if self._device_list.get(key,InverterData).enabled:
207
+ self._device_list[key].status = str_value
208
+
209
+ self._device_list[key].last_event = await self.client.get_device_last_event(key)
210
+
188
211
  _LOGGER.debug("Inverter data updated: %s",self._device_list)
189
212
 
190
213
  return self._device_list
@@ -1,11 +1,12 @@
1
1
  """Models for SolarLog."""
2
2
  from dataclasses import dataclass, field
3
- from datetime import datetime
3
+ from datetime import date, datetime
4
4
 
5
5
  from mashumaro import DataClassDictMixin
6
6
 
7
+
7
8
  @dataclass
8
- class BatteryData():
9
+ class BatteryData:
9
10
  """Battery Data model."""
10
11
 
11
12
  charge_power: float = 0
@@ -14,20 +15,34 @@ class BatteryData():
14
15
  voltage: float = 0
15
16
 
16
17
  @dataclass
17
- class EnergyData():
18
+ class EnergyData:
18
19
  """Energy Data model."""
19
20
 
20
21
  production: float | None = None
21
22
  self_consumption: float | None = None
22
23
 
23
24
  @dataclass
24
- class InverterData():
25
+ class EventData:
26
+ """Event Data model."""
27
+
28
+ end: datetime
29
+ error: int
30
+ event: int
31
+ start: datetime
32
+
33
+ @dataclass
34
+ class InverterData:
25
35
  """Inverter Data model."""
36
+ # pylint: disable=too-many-instance-attributes
26
37
 
27
38
  name: str = ""
28
- enabled: bool = False
29
- current_power: float | None = None
30
39
  consumption_year: float | None = None
40
+ current_power: float | None = None
41
+ enabled: bool = False
42
+ errors: dict[int,str] = field(default_factory=dict)
43
+ events: dict[int,str] = field(default_factory=dict)
44
+ last_event: EventData | None = None
45
+ status: str | None = None
31
46
 
32
47
 
33
48
  @dataclass
@@ -63,6 +78,8 @@ class SolarlogData(DataClassDictMixin):
63
78
 
64
79
  #extended data
65
80
  battery_data: BatteryData | None = None
81
+ firmware_release_date: date | None = None
82
+ firmware_version: str | None = None
66
83
  inverter_data: dict[int, InverterData] = field(default_factory=dict)
67
84
  production_year: float | None = None
68
85
  self_consumption_year: float | None = None