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.
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.devcontainer/devcontainer.json +3 -3
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.github/workflows/python-publish.yml +1 -1
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.github/workflows/tests.yml +1 -1
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.gitignore +1 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/PKG-INFO +3 -3
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/pyproject.toml +2 -2
- solarlog_cli-0.7.2/pytest.ini +3 -0
- solarlog_cli-0.7.2/requirements.txt +6 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_client.py +85 -32
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_connector.py +37 -14
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_models.py +23 -6
- solarlog_cli-0.7.2/tests/__snapshots__/test_solarlog_cli.ambr +180 -0
- solarlog_cli-0.7.2/tests/conftest.py +29 -0
- solarlog_cli-0.7.2/tests/fixtures/device_data_1.json +16 -0
- solarlog_cli-0.7.2/tests/fixtures/device_data_2.json +16 -0
- solarlog_cli-0.7.2/tests/fixtures/device_data_3.json +16 -0
- solarlog_cli-0.7.2/tests/fixtures/device_data_4.json +16 -0
- solarlog_cli-0.7.2/tests/fixtures/device_error_list.json +11 -0
- solarlog_cli-0.7.2/tests/fixtures/device_event_list_1.json +12 -0
- solarlog_cli-0.7.2/tests/fixtures/device_event_list_2.json +12 -0
- solarlog_cli-0.7.2/tests/fixtures/device_event_list_3.json +12 -0
- solarlog_cli-0.7.2/tests/fixtures/device_event_list_4.json +12 -0
- solarlog_cli-0.7.2/tests/fixtures/device_last_event.json +27 -0
- solarlog_cli-0.7.2/tests/fixtures/device_status.json +9 -0
- solarlog_cli-0.7.2/tests/fixtures/firmware_data.json +6 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/ruff.toml +1 -1
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/syrupy.py +1 -2
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/test_solarlog_cli.py +115 -52
- solarlog_cli-0.7.1/.coverage +0 -0
- solarlog_cli-0.7.1/pytest.ini +0 -2
- solarlog_cli-0.7.1/requirements.txt +0 -7
- solarlog_cli-0.7.1/tests/__snapshots__/test_solarlog_cli.ambr +0 -95
- solarlog_cli-0.7.1/tests/conftest.py +0 -22
- solarlog_cli-0.7.1/tests/fixtures/device_data_1.json +0 -7
- solarlog_cli-0.7.1/tests/fixtures/device_data_2.json +0 -7
- solarlog_cli-0.7.1/tests/fixtures/device_data_3.json +0 -7
- solarlog_cli-0.7.1/tests/fixtures/device_data_4.json +0 -7
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.github/workflows/pylint.yml +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.github/workflows/ruff.yml +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.github/workflows/typing.yml +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.vscode/settings.json +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/.vscode/tasks.json +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/LICENSE +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/README.md +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/src/solarlog_cli/__init__.py +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/src/solarlog_cli/py.typed +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_exceptions.py +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/__init__.py +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/basic_data.json +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/basic_data_during_update.json +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/basic_data_no_power.json +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/battery_data.json +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/battery_data_without_battery.json +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/device_list.json +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/device_list_access_denied.json +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/energy_per_inverter.json +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/extended_data.json +0 -0
- {solarlog_cli-0.7.1 → solarlog_cli-0.7.2}/tests/fixtures/power_per_inverter.json +0 -0
- {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/
|
|
6
|
+
"image": "mcr.microsoft.com/devcontainers/python:3-3.14-bookworm",
|
|
7
7
|
|
|
8
8
|
"features": {
|
|
9
|
-
"ghcr.io/devcontainers/features/docker-in-docker:
|
|
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:
|
|
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@
|
|
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-
|
|
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,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: solarlog_cli
|
|
3
|
-
Version: 0.7.
|
|
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.
|
|
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
|
]
|
|
@@ -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 = ""
|
|
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
|
-
|
|
60
|
-
return True
|
|
61
|
-
|
|
62
|
-
return False
|
|
63
|
+
return response.status == 200
|
|
63
64
|
|
|
64
65
|
async def login(self) -> bool:
|
|
65
|
-
"""
|
|
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:
|
|
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
|
|
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
|
|
278
|
-
"""Get
|
|
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):
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
178
|
-
for key, value in
|
|
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
|
-
|
|
184
|
-
for key, value in
|
|
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
|
|
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
|