solarlog-cli 0.6.1__tar.gz → 0.7.1__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.6.1 → solarlog_cli-0.7.1}/.coverage +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.github/workflows/pylint.yml +1 -1
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.github/workflows/python-publish.yml +1 -1
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.github/workflows/ruff.yml +1 -1
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.github/workflows/tests.yml +2 -2
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.github/workflows/typing.yml +1 -1
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/PKG-INFO +1 -1
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/pyproject.toml +1 -1
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/requirements.txt +1 -1
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/src/solarlog_cli/solarlog_client.py +30 -19
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/src/solarlog_cli/solarlog_connector.py +36 -19
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/src/solarlog_cli/solarlog_models.py +7 -1
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/__snapshots__/test_solarlog_cli.ambr +6 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/test_solarlog_cli.py +32 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.devcontainer/devcontainer.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.gitignore +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.vscode/settings.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.vscode/tasks.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/LICENSE +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/README.md +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/pytest.ini +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/src/solarlog_cli/__init__.py +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/src/solarlog_cli/py.typed +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/src/solarlog_cli/solarlog_exceptions.py +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/__init__.py +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/conftest.py +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/basic_data.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/basic_data_during_update.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/basic_data_no_power.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/battery_data.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/battery_data_without_battery.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/device_data_1.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/device_data_2.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/device_data_3.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/device_data_4.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/device_list.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/device_list_access_denied.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/energy_per_inverter.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/extended_data.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/power_per_inverter.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/user_salts.json +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/ruff.toml +0 -0
- {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/syrupy.py +0 -0
|
Binary file
|
|
@@ -9,7 +9,7 @@ on:
|
|
|
9
9
|
workflow_dispatch:
|
|
10
10
|
|
|
11
11
|
env:
|
|
12
|
-
DEFAULT_PYTHON: "3.
|
|
12
|
+
DEFAULT_PYTHON: "3.13"
|
|
13
13
|
|
|
14
14
|
jobs:
|
|
15
15
|
pytest:
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
runs-on: ubuntu-latest
|
|
18
18
|
strategy:
|
|
19
19
|
matrix:
|
|
20
|
-
python: ["3.
|
|
20
|
+
python: ["3.13"]
|
|
21
21
|
steps:
|
|
22
22
|
- name: ⤵️ Check out code from GitHub
|
|
23
23
|
uses: actions/checkout@v4.1.7
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solarlog_cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
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
|
|
@@ -17,7 +17,7 @@ from .solarlog_exceptions import (
|
|
|
17
17
|
SolarLogUpdateError,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
from .solarlog_models import SolarlogData
|
|
20
|
+
from .solarlog_models import EnergyData, SolarlogData
|
|
21
21
|
|
|
22
22
|
SOLARLOG_REQUEST_PAYLOAD = '{ "801": { "170": null } }'
|
|
23
23
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -41,6 +41,7 @@ class Client:
|
|
|
41
41
|
|
|
42
42
|
self._hashed_pwd: bool = False
|
|
43
43
|
self._close_session: bool = True
|
|
44
|
+
self._token = "" #required for older firmware, where cookie is not sufficient
|
|
44
45
|
|
|
45
46
|
async def test_connection(self) -> bool:
|
|
46
47
|
"""Test the connection to Solar-Log."""
|
|
@@ -111,15 +112,20 @@ class Client:
|
|
|
111
112
|
|
|
112
113
|
self._hashed_pwd = True
|
|
113
114
|
|
|
114
|
-
self.session.cookie_jar.update_cookies(
|
|
115
|
+
self.session.cookie_jar.update_cookies(
|
|
116
|
+
{"SolarLog": response.cookies["SolarLog"].value})
|
|
117
|
+
|
|
118
|
+
if not self._hashed_pwd:
|
|
119
|
+
self._token = response.cookies["SolarLog"].value
|
|
115
120
|
|
|
116
121
|
_LOGGER.debug("response: %s", text)
|
|
117
122
|
_LOGGER.debug("cookies: %s", response.cookies)
|
|
118
|
-
_LOGGER.debug("Login successful, token: %s",
|
|
123
|
+
_LOGGER.debug("Login successful, token: %s",
|
|
124
|
+
response.cookies["SolarLog"].value)
|
|
119
125
|
|
|
120
126
|
return True
|
|
121
127
|
|
|
122
|
-
async def execute_http_request(self, body: str, path: str = "getjp") -> ClientResponse:
|
|
128
|
+
async def execute_http_request(self, body: str, path: str = "getjp", timeout: float | None = None) -> ClientResponse: # pylint: disable=line-too-long
|
|
123
129
|
"""Helper function to process the HTTP Get call."""
|
|
124
130
|
if self.session is None:
|
|
125
131
|
self.session = ClientSession()
|
|
@@ -129,6 +135,9 @@ class Client:
|
|
|
129
135
|
|
|
130
136
|
header = {"Content-Type": "text/html", "X-SL-CSRF-PROTECTION": "1"}
|
|
131
137
|
|
|
138
|
+
if self._token != "":
|
|
139
|
+
body = f"token={self._token}; " + body
|
|
140
|
+
|
|
132
141
|
_LOGGER.debug("HTTP-request header: %s", header)
|
|
133
142
|
_LOGGER.debug("HTTP-request body: %s", body)
|
|
134
143
|
|
|
@@ -137,7 +146,7 @@ class Client:
|
|
|
137
146
|
url=url,
|
|
138
147
|
headers=header,
|
|
139
148
|
data=body,
|
|
140
|
-
timeout=ClientTimeout(total=self.request_timeout),
|
|
149
|
+
timeout=ClientTimeout(total=timeout or self.request_timeout),
|
|
141
150
|
)
|
|
142
151
|
except asyncio.TimeoutError as exception:
|
|
143
152
|
msg = f"Timeout occurred while connecting to Solar-Log at {self.host}"
|
|
@@ -211,22 +220,22 @@ class Client:
|
|
|
211
220
|
|
|
212
221
|
return data
|
|
213
222
|
|
|
214
|
-
async def get_battery_data(self) -> list[float]:
|
|
223
|
+
async def get_battery_data(self, timeout: float | None = None) -> list[float]:
|
|
215
224
|
"""Get battery data from Solar-Log"""
|
|
216
225
|
|
|
217
226
|
raw_data: dict = await self.parse_http_response(
|
|
218
|
-
await self.execute_http_request('{ "858": null }')
|
|
227
|
+
await self.execute_http_request('{ "858": null }', timeout=timeout)
|
|
219
228
|
)
|
|
220
229
|
|
|
221
230
|
data: list[float] = raw_data["858"]
|
|
222
231
|
|
|
223
232
|
return data
|
|
224
233
|
|
|
225
|
-
async def get_power_per_inverter(self) -> dict[int, float]:
|
|
234
|
+
async def get_power_per_inverter(self, timeout: float | None = None) -> dict[int, float]:
|
|
226
235
|
"""Get power data from Solar-Log"""
|
|
227
236
|
|
|
228
237
|
raw_data: dict = await self.parse_http_response(
|
|
229
|
-
await self.execute_http_request('{ "782": null }')
|
|
238
|
+
await self.execute_http_request('{ "782": null }', timeout=timeout)
|
|
230
239
|
)
|
|
231
240
|
|
|
232
241
|
data = {int(key): val for key,
|
|
@@ -234,11 +243,11 @@ class Client:
|
|
|
234
243
|
|
|
235
244
|
return data
|
|
236
245
|
|
|
237
|
-
async def get_energy_per_inverter(self) -> dict[int, float]:
|
|
246
|
+
async def get_energy_per_inverter(self, timeout: float | None = None) -> dict[int, float]:
|
|
238
247
|
"""Get power data from Solar-Log"""
|
|
239
248
|
|
|
240
249
|
raw_data: dict = await self.parse_http_response(
|
|
241
|
-
await self.execute_http_request('{ "854": null }')
|
|
250
|
+
await self.execute_http_request('{ "854": null }', timeout=timeout)
|
|
242
251
|
)
|
|
243
252
|
data_list = raw_data["854"][-1][-1]
|
|
244
253
|
|
|
@@ -250,25 +259,27 @@ class Client:
|
|
|
250
259
|
|
|
251
260
|
return data
|
|
252
261
|
|
|
253
|
-
async def get_energy(self,
|
|
262
|
+
async def get_energy(self, timeout: float | None = None) -> EnergyData | None:
|
|
254
263
|
"""Get energy data from Solar-Log"""
|
|
255
264
|
|
|
256
265
|
raw_data: dict = await self.parse_http_response(
|
|
257
|
-
await self.execute_http_request('{ "878": null }')
|
|
266
|
+
await self.execute_http_request('{ "878": null }', timeout=timeout)
|
|
258
267
|
)
|
|
259
268
|
|
|
260
269
|
if raw_data["878"] != "QUERY IMPOSSIBLE 000":
|
|
261
|
-
|
|
262
|
-
|
|
270
|
+
return EnergyData(
|
|
271
|
+
production=raw_data["878"][-1][1],
|
|
272
|
+
self_consumption=raw_data["878"][-1][3]
|
|
273
|
+
)
|
|
263
274
|
|
|
264
|
-
return
|
|
275
|
+
return None
|
|
265
276
|
|
|
266
|
-
async def get_device_list(self) -> dict[int, str]:
|
|
277
|
+
async def get_device_list(self, timeout: float | None = None) -> dict[int, str]:
|
|
267
278
|
"""Get list of all connected devices."""
|
|
268
279
|
|
|
269
280
|
# get list of all inverters connected to Solar-Log
|
|
270
281
|
raw_data: dict = await self.parse_http_response(
|
|
271
|
-
await self.execute_http_request('{ "740": null }')
|
|
282
|
+
await self.execute_http_request('{ "740": null }', timeout=timeout)
|
|
272
283
|
)
|
|
273
284
|
raw_data = raw_data["740"]
|
|
274
285
|
|
|
@@ -279,7 +290,7 @@ class Client:
|
|
|
279
290
|
# get name of the inverter
|
|
280
291
|
raw_data = await self.parse_http_response(
|
|
281
292
|
await self.execute_http_request(
|
|
282
|
-
f"""{{ "141": {{ "{key}": {{ "119": null }} }} }}"""
|
|
293
|
+
f"""{{ "141": {{ "{key}": {{ "119": null }} }} }}""", timeout=timeout
|
|
283
294
|
)
|
|
284
295
|
)
|
|
285
296
|
device_list |= {int(key): raw_data["141"][key]["119"]}
|
|
@@ -12,7 +12,7 @@ from .solarlog_exceptions import(
|
|
|
12
12
|
SolarLogConnectionError,
|
|
13
13
|
SolarLogUpdateError,
|
|
14
14
|
)
|
|
15
|
-
from .solarlog_models import BatteryData, InverterData, SolarlogData
|
|
15
|
+
from .solarlog_models import BatteryData, EnergyData, InverterData, SolarlogData
|
|
16
16
|
|
|
17
17
|
_LOGGER = logging.getLogger(__name__)
|
|
18
18
|
|
|
@@ -82,7 +82,28 @@ class SolarLogConnector:
|
|
|
82
82
|
return await self.client.login()
|
|
83
83
|
|
|
84
84
|
async def update_data(self) -> SolarlogData:
|
|
85
|
-
"""Get data from Solar-Log."""
|
|
85
|
+
"""Get all data from Solar-Log."""
|
|
86
|
+
|
|
87
|
+
data: SolarlogData = await self.update_basic_data()
|
|
88
|
+
|
|
89
|
+
if self.extended_data:
|
|
90
|
+
energy_data: EnergyData | None = await self.client.get_energy()
|
|
91
|
+
|
|
92
|
+
if energy_data is not None:
|
|
93
|
+
data.production_year = energy_data.production
|
|
94
|
+
data.self_consumption_year = energy_data.self_consumption
|
|
95
|
+
|
|
96
|
+
if self._device_list != {}:
|
|
97
|
+
data.inverter_data = await self.update_inverter_data()
|
|
98
|
+
|
|
99
|
+
data.battery_data = await self.update_battery_data()
|
|
100
|
+
|
|
101
|
+
_LOGGER.debug("Extended data updated: %s",data)
|
|
102
|
+
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
async def update_basic_data(self) -> SolarlogData:
|
|
106
|
+
"""Get basic data from Solar-Log."""
|
|
86
107
|
|
|
87
108
|
data: SolarlogData = await self.client.get_basic_data()
|
|
88
109
|
|
|
@@ -94,15 +115,6 @@ class SolarLogConnector:
|
|
|
94
115
|
data.last_updated = data.last_updated.replace(tzinfo=self.timezone)
|
|
95
116
|
|
|
96
117
|
_LOGGER.debug("Basic data updated: %s",data)
|
|
97
|
-
if self.extended_data:
|
|
98
|
-
data = await self.client.get_energy(data)
|
|
99
|
-
|
|
100
|
-
if self._device_list != {}:
|
|
101
|
-
data.inverter_data = await self.update_inverter_data()
|
|
102
|
-
|
|
103
|
-
data.battery_data = await self.update_battery_data()
|
|
104
|
-
|
|
105
|
-
_LOGGER.debug("Extended data updated: %s",data)
|
|
106
118
|
|
|
107
119
|
#calculated values (for downward compatibility)
|
|
108
120
|
data.alternator_loss = data.power_dc - data.power_ac
|
|
@@ -119,11 +131,10 @@ class SolarLogConnector:
|
|
|
119
131
|
|
|
120
132
|
return data
|
|
121
133
|
|
|
122
|
-
|
|
123
|
-
async def update_battery_data(self) -> BatteryData | None:
|
|
134
|
+
async def update_battery_data(self, timeout: float | None = None) -> BatteryData | None:
|
|
124
135
|
"""Update device specific data."""
|
|
125
136
|
|
|
126
|
-
raw_data = await self.client.get_battery_data()
|
|
137
|
+
raw_data = await self.client.get_battery_data(timeout)
|
|
127
138
|
|
|
128
139
|
if raw_data == []:
|
|
129
140
|
return None
|
|
@@ -139,12 +150,18 @@ class SolarLogConnector:
|
|
|
139
150
|
|
|
140
151
|
return battery_data
|
|
141
152
|
|
|
142
|
-
async def
|
|
153
|
+
async def update_energy_data(self, timeout: float | None = None) -> EnergyData | None:
|
|
154
|
+
"""Update energy data (production and self consumption)."""
|
|
155
|
+
|
|
156
|
+
return await self.client.get_energy(timeout)
|
|
157
|
+
|
|
158
|
+
async def update_device_list(self, timeout: float | None = None) -> dict[int, InverterData]:
|
|
143
159
|
"""Update list of devices."""
|
|
160
|
+
|
|
144
161
|
if not self.extended_data:
|
|
145
162
|
return {}
|
|
146
163
|
|
|
147
|
-
devices = await self.client.get_device_list()
|
|
164
|
+
devices = await self.client.get_device_list(timeout)
|
|
148
165
|
|
|
149
166
|
self._device_list = {
|
|
150
167
|
key: InverterData(name=value,enabled=self.device(key).enabled)
|
|
@@ -154,16 +171,16 @@ class SolarLogConnector:
|
|
|
154
171
|
|
|
155
172
|
return self._device_list
|
|
156
173
|
|
|
157
|
-
async def update_inverter_data(self) -> dict[int, InverterData]:
|
|
174
|
+
async def update_inverter_data(self, timeout: float | None = None) -> dict[int, InverterData]:
|
|
158
175
|
"""Update device specific data."""
|
|
159
176
|
|
|
160
|
-
raw_data = await self.client.get_power_per_inverter()
|
|
177
|
+
raw_data = await self.client.get_power_per_inverter(timeout)
|
|
161
178
|
for key, value in raw_data.items():
|
|
162
179
|
key = int(key)
|
|
163
180
|
if self._device_list.get(key,InverterData).enabled:
|
|
164
181
|
self._device_list[key].current_power = float(value)
|
|
165
182
|
|
|
166
|
-
raw_data = await self.client.get_energy_per_inverter()
|
|
183
|
+
raw_data = await self.client.get_energy_per_inverter(timeout)
|
|
167
184
|
for key, value in raw_data.items():
|
|
168
185
|
if self._device_list.get(key,InverterData).enabled:
|
|
169
186
|
self._device_list[key].consumption_year = float(value)
|
|
@@ -13,6 +13,12 @@ class BatteryData():
|
|
|
13
13
|
level: float = 0
|
|
14
14
|
voltage: float = 0
|
|
15
15
|
|
|
16
|
+
@dataclass
|
|
17
|
+
class EnergyData():
|
|
18
|
+
"""Energy Data model."""
|
|
19
|
+
|
|
20
|
+
production: float | None = None
|
|
21
|
+
self_consumption: float | None = None
|
|
16
22
|
|
|
17
23
|
@dataclass
|
|
18
24
|
class InverterData():
|
|
@@ -21,7 +27,7 @@ class InverterData():
|
|
|
21
27
|
name: str = ""
|
|
22
28
|
enabled: bool = False
|
|
23
29
|
current_power: float | None = None
|
|
24
|
-
consumption_year: float | None= None
|
|
30
|
+
consumption_year: float | None = None
|
|
25
31
|
|
|
26
32
|
|
|
27
33
|
@dataclass
|
|
@@ -297,6 +297,38 @@ async def test_update_data_without_battery(
|
|
|
297
297
|
await solarlog_connector.client.close()
|
|
298
298
|
assert solarlog_connector.client.session.closed
|
|
299
299
|
|
|
300
|
+
async def test_update_energy_data(
|
|
301
|
+
responses: aioresponses,
|
|
302
|
+
snapshot: SnapshotAssertion
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Test update energy data."""
|
|
305
|
+
|
|
306
|
+
responses.post(
|
|
307
|
+
"http://solarlog.com/getjp",
|
|
308
|
+
body=load_fixture("extended_data.json"),
|
|
309
|
+
)
|
|
310
|
+
responses.post(
|
|
311
|
+
"http://solarlog.com/getjp",
|
|
312
|
+
body='{"878": "QUERY IMPOSSIBLE 000"}',
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
solarlog_connector = SolarLogConnector(
|
|
316
|
+
"http://solarlog.com",
|
|
317
|
+
True,
|
|
318
|
+
"UTC",
|
|
319
|
+
{0: True, 1: True, 2: False, 3: True},
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
data = await solarlog_connector.update_energy_data()
|
|
323
|
+
|
|
324
|
+
assert data == snapshot
|
|
325
|
+
|
|
326
|
+
data = await solarlog_connector.update_energy_data()
|
|
327
|
+
|
|
328
|
+
assert data is None
|
|
329
|
+
|
|
330
|
+
await solarlog_connector.client.close()
|
|
331
|
+
assert solarlog_connector.client.session.closed
|
|
300
332
|
|
|
301
333
|
@pytest.mark.parametrize(
|
|
302
334
|
("status", "request_timeout", "error"),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|