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.
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.devcontainer/devcontainer.json +3 -3
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.github/workflows/python-publish.yml +1 -1
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.github/workflows/tests.yml +1 -1
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.gitignore +1 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/PKG-INFO +3 -3
- {solarlog_cli-0.7.0 → 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.0 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_client.py +91 -31
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_connector.py +37 -14
- {solarlog_cli-0.7.0 → 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.0 → solarlog_cli-0.7.2}/tests/ruff.toml +1 -1
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/syrupy.py +1 -2
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/test_solarlog_cli.py +115 -52
- solarlog_cli-0.7.0/.coverage +0 -0
- solarlog_cli-0.7.0/pytest.ini +0 -2
- solarlog_cli-0.7.0/requirements.txt +0 -7
- solarlog_cli-0.7.0/tests/__snapshots__/test_solarlog_cli.ambr +0 -95
- solarlog_cli-0.7.0/tests/conftest.py +0 -22
- solarlog_cli-0.7.0/tests/fixtures/device_data_1.json +0 -7
- solarlog_cli-0.7.0/tests/fixtures/device_data_2.json +0 -7
- solarlog_cli-0.7.0/tests/fixtures/device_data_3.json +0 -7
- solarlog_cli-0.7.0/tests/fixtures/device_data_4.json +0 -7
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.github/workflows/pylint.yml +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.github/workflows/ruff.yml +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.github/workflows/typing.yml +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.vscode/settings.json +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/.vscode/tasks.json +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/LICENSE +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/README.md +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/src/solarlog_cli/__init__.py +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/src/solarlog_cli/py.typed +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/src/solarlog_cli/solarlog_exceptions.py +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/__init__.py +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/basic_data.json +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/basic_data_during_update.json +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/basic_data_no_power.json +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/battery_data.json +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/battery_data_without_battery.json +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/device_list.json +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/device_list_access_denied.json +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/energy_per_inverter.json +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/extended_data.json +0 -0
- {solarlog_cli-0.7.0 → solarlog_cli-0.7.2}/tests/fixtures/power_per_inverter.json +0 -0
- {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/
|
|
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,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
|
-
|
|
59
|
-
return True
|
|
60
|
-
|
|
61
|
-
return False
|
|
63
|
+
return response.status == 200
|
|
62
64
|
|
|
63
65
|
async def login(self) -> bool:
|
|
64
|
-
"""
|
|
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:
|
|
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
|
-
|
|
135
|
-
|
|
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
|
|
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
|
|
271
|
-
"""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."""
|
|
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):
|
|
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
|
|
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
|