solarlog-cli 0.7.0__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.0 → solarlog_cli-0.7.2}/.devcontainer/devcontainer.json +3 -3
  2. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.github/workflows/python-publish.yml +1 -1
  3. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.github/workflows/tests.yml +1 -1
  4. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.gitignore +1 -0
  5. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/PKG-INFO +3 -3
  6. {solarlog_cli-0.7.0 → 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.0 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_client.py +91 -31
  10. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_connector.py +37 -14
  11. {solarlog_cli-0.7.0 → 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.0 → solarlog_cli-0.7.2}/tests/ruff.toml +1 -1
  27. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/syrupy.py +1 -2
  28. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/test_solarlog_cli.py +115 -52
  29. solarlog_cli-0.7.0/.coverage +0 -0
  30. solarlog_cli-0.7.0/pytest.ini +0 -2
  31. solarlog_cli-0.7.0/requirements.txt +0 -7
  32. solarlog_cli-0.7.0/tests/__snapshots__/test_solarlog_cli.ambr +0 -95
  33. solarlog_cli-0.7.0/tests/conftest.py +0 -22
  34. solarlog_cli-0.7.0/tests/fixtures/device_data_1.json +0 -7
  35. solarlog_cli-0.7.0/tests/fixtures/device_data_2.json +0 -7
  36. solarlog_cli-0.7.0/tests/fixtures/device_data_3.json +0 -7
  37. solarlog_cli-0.7.0/tests/fixtures/device_data_4.json +0 -7
  38. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.github/workflows/pylint.yml +0 -0
  39. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.github/workflows/ruff.yml +0 -0
  40. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.github/workflows/typing.yml +0 -0
  41. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.vscode/settings.json +0 -0
  42. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.vscode/tasks.json +0 -0
  43. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/LICENSE +0 -0
  44. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/README.md +0 -0
  45. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/src/solarlog_cli/__init__.py +0 -0
  46. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/src/solarlog_cli/py.typed +0 -0
  47. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_exceptions.py +0 -0
  48. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/__init__.py +0 -0
  49. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/basic_data.json +0 -0
  50. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/basic_data_during_update.json +0 -0
  51. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/basic_data_no_power.json +0 -0
  52. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/battery_data.json +0 -0
  53. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/battery_data_without_battery.json +0 -0
  54. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/device_list.json +0 -0
  55. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/device_list_access_denied.json +0 -0
  56. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/energy_per_inverter.json +0 -0
  57. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/extended_data.json +0 -0
  58. {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/power_per_inverter.json +0 -0
  59. {solarlog_cli-0.7.0 → 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.0
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.0"
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,6 +45,7 @@ class Client:
41
45
 
42
46
  self._hashed_pwd: bool = False
43
47
  self._close_session: bool = True
48
+ self._token = "" # required for older firmware, where cookie is not sufficient
44
49
 
45
50
  async def test_connection(self) -> bool:
46
51
  """Test the connection to Solar-Log."""
@@ -55,13 +60,10 @@ class Client:
55
60
  timeout=ClientTimeout(total=self.request_timeout)
56
61
  )
57
62
 
58
- if response.status == 200:
59
- return True
60
-
61
- return False
63
+ return response.status == 200
62
64
 
63
65
  async def login(self) -> bool:
64
- """Test the connection to Solar-Log."""
66
+ """Login to Solar-Log."""
65
67
 
66
68
  if self.password == "":
67
69
  return False
@@ -71,7 +73,7 @@ class Client:
71
73
  response = await self.execute_http_request(payload, "login")
72
74
 
73
75
  text = await response.text()
74
- _LOGGER.debug("Response: %s", text)
76
+ _LOGGER.debug("Response to request for login: %s", text)
75
77
  if text.count("FAILED - User was wrong"):
76
78
  # Response means, that no password is required
77
79
  self.password = ""
@@ -114,6 +116,9 @@ class Client:
114
116
  self.session.cookie_jar.update_cookies(
115
117
  {"SolarLog": response.cookies["SolarLog"].value})
116
118
 
119
+ if not self._hashed_pwd:
120
+ self._token = response.cookies["SolarLog"].value
121
+
117
122
  _LOGGER.debug("response: %s", text)
118
123
  _LOGGER.debug("cookies: %s", response.cookies)
119
124
  _LOGGER.debug("Login successful, token: %s",
@@ -121,7 +126,7 @@ class Client:
121
126
 
122
127
  return True
123
128
 
124
- 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
125
130
  """Helper function to process the HTTP Get call."""
126
131
  if self.session is None:
127
132
  self.session = ClientSession()
@@ -131,8 +136,11 @@ class Client:
131
136
 
132
137
  header = {"Content-Type": "text/html", "X-SL-CSRF-PROTECTION": "1"}
133
138
 
134
- _LOGGER.debug("HTTP-request header: %s", header)
135
- _LOGGER.debug("HTTP-request body: %s", body)
139
+ if self._token != "":
140
+ body = f"token={self._token}; " + body
141
+
142
+ # _LOGGER.debug("HTTP-request header: %s", header)
143
+ # _LOGGER.debug("HTTP-request body: %s", body)
136
144
 
137
145
  try:
138
146
  response = await self.session.post(
@@ -141,7 +149,7 @@ class Client:
141
149
  data=body,
142
150
  timeout=ClientTimeout(total=timeout or self.request_timeout),
143
151
  )
144
- except asyncio.TimeoutError as exception:
152
+ except TimeoutError as exception:
145
153
  msg = f"Timeout occurred while connecting to Solar-Log at {self.host}"
146
154
  raise SolarLogConnectionError(msg) from exception
147
155
 
@@ -156,14 +164,14 @@ class Client:
156
164
  {"Content-Type": content_type, "response": text},
157
165
  )
158
166
 
159
- _LOGGER.debug("HTTP-request successful: %s", response)
167
+ # _LOGGER.debug("HTTP-request successful: %s", response)
160
168
  return response
161
169
 
162
170
  async def parse_http_response(self, response: ClientResponse) -> dict[str, Any]:
163
171
  """Helper function to parse the HTTP response."""
164
172
 
165
173
  text = await response.text(errors="replace")
166
- _LOGGER.debug("Parsing http response: %s", text)
174
+ # _LOGGER.debug("Parsing http response: %s", text)
167
175
 
168
176
  if text.count('{"QUERY IMPOSSIBLE 000"}'):
169
177
  raise SolarLogUpdateError(f"Server response: {text}")
@@ -192,7 +200,7 @@ class Client:
192
200
 
193
201
  data = SolarlogData(
194
202
  last_updated=datetime.strptime(
195
- raw_data["100"], "%d.%m.%y %H:%M:%S"),
203
+ raw_data["100"], "%d.%m.%y %H:%M:%S").astimezone(self.timezone),
196
204
  power_ac=raw_data["101"],
197
205
  power_dc=raw_data["102"],
198
206
  voltage_ac=raw_data["103"],
@@ -214,7 +222,7 @@ class Client:
214
222
  return data
215
223
 
216
224
  async def get_battery_data(self, timeout: float | None = None) -> list[float]:
217
- """Get battery data from Solar-Log"""
225
+ """Get battery data from Solar-Log."""
218
226
 
219
227
  raw_data: dict = await self.parse_http_response(
220
228
  await self.execute_http_request('{ "858": null }', timeout=timeout)
@@ -225,7 +233,7 @@ class Client:
225
233
  return data
226
234
 
227
235
  async def get_power_per_inverter(self, timeout: float | None = None) -> dict[int, float]:
228
- """Get power data from Solar-Log"""
236
+ """Get power data from Solar-Log."""
229
237
 
230
238
  raw_data: dict = await self.parse_http_response(
231
239
  await self.execute_http_request('{ "782": null }', timeout=timeout)
@@ -237,7 +245,7 @@ class Client:
237
245
  return data
238
246
 
239
247
  async def get_energy_per_inverter(self, timeout: float | None = None) -> dict[int, float]:
240
- """Get power data from Solar-Log"""
248
+ """Get power data from Solar-Log."""
241
249
 
242
250
  raw_data: dict = await self.parse_http_response(
243
251
  await self.execute_http_request('{ "854": null }', timeout=timeout)
@@ -253,7 +261,7 @@ class Client:
253
261
  return data
254
262
 
255
263
  async def get_energy(self, timeout: float | None = None) -> EnergyData | None:
256
- """Get energy data from Solar-Log"""
264
+ """Get energy data from Solar-Log."""
257
265
 
258
266
  raw_data: dict = await self.parse_http_response(
259
267
  await self.execute_http_request('{ "878": null }', timeout=timeout)
@@ -267,8 +275,26 @@ class Client:
267
275
 
268
276
  return None
269
277
 
270
- async def get_device_list(self, timeout: float | None = None) -> dict[int, str]:
271
- """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."""
272
298
 
273
299
  # get list of all inverters connected to Solar-Log
274
300
  raw_data: dict = await self.parse_http_response(
@@ -276,20 +302,54 @@ class Client:
276
302
  )
277
303
  raw_data = raw_data["740"]
278
304
 
279
- device_list: dict[int, str] = {}
305
+ device_list: dict[int, tuple[str, dict, dict]] = {}
280
306
 
281
307
  for key, value in raw_data.items():
282
308
  if value != "Err":
283
- # 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
+ )
284
314
  raw_data = await self.parse_http_response(
285
- await self.execute_http_request(
286
- f"""{{ "141": {{ "{key}": {{ "119": null }} }} }}""", timeout=timeout
287
- )
315
+ await self.execute_http_request(query, timeout=timeout)
288
316
  )
289
- 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
+ }
290
323
 
291
324
  return device_list
292
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
+
293
353
  async def close(self) -> None:
294
354
  """Close open client session."""
295
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