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.
Files changed (43) hide show
  1. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.coverage +0 -0
  2. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.github/workflows/pylint.yml +1 -1
  3. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.github/workflows/python-publish.yml +1 -1
  4. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.github/workflows/ruff.yml +1 -1
  5. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.github/workflows/tests.yml +2 -2
  6. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.github/workflows/typing.yml +1 -1
  7. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/PKG-INFO +1 -1
  8. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/pyproject.toml +1 -1
  9. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/requirements.txt +1 -1
  10. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/src/solarlog_cli/solarlog_client.py +30 -19
  11. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/src/solarlog_cli/solarlog_connector.py +36 -19
  12. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/src/solarlog_cli/solarlog_models.py +7 -1
  13. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/__snapshots__/test_solarlog_cli.ambr +6 -0
  14. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/test_solarlog_cli.py +32 -0
  15. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.devcontainer/devcontainer.json +0 -0
  16. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.gitignore +0 -0
  17. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.vscode/settings.json +0 -0
  18. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/.vscode/tasks.json +0 -0
  19. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/LICENSE +0 -0
  20. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/README.md +0 -0
  21. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/pytest.ini +0 -0
  22. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/src/solarlog_cli/__init__.py +0 -0
  23. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/src/solarlog_cli/py.typed +0 -0
  24. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/src/solarlog_cli/solarlog_exceptions.py +0 -0
  25. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/__init__.py +0 -0
  26. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/conftest.py +0 -0
  27. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/basic_data.json +0 -0
  28. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/basic_data_during_update.json +0 -0
  29. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/basic_data_no_power.json +0 -0
  30. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/battery_data.json +0 -0
  31. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/battery_data_without_battery.json +0 -0
  32. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/device_data_1.json +0 -0
  33. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/device_data_2.json +0 -0
  34. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/device_data_3.json +0 -0
  35. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/device_data_4.json +0 -0
  36. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/device_list.json +0 -0
  37. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/device_list_access_denied.json +0 -0
  38. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/energy_per_inverter.json +0 -0
  39. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/extended_data.json +0 -0
  40. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/power_per_inverter.json +0 -0
  41. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/fixtures/user_salts.json +0 -0
  42. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/ruff.toml +0 -0
  43. {solarlog_cli-0.6.1 → solarlog_cli-0.7.1}/tests/syrupy.py +0 -0
@@ -7,7 +7,7 @@ jobs:
7
7
  runs-on: ubuntu-latest
8
8
  strategy:
9
9
  matrix:
10
- python-version: ["3.12"]
10
+ python-version: ["3.13"]
11
11
  steps:
12
12
  - name: ⤵️ Checkout the repository
13
13
  uses: actions/checkout@v4.1.7
@@ -25,7 +25,7 @@ jobs:
25
25
  - name: Set up Python
26
26
  uses: actions/setup-python@v3
27
27
  with:
28
- python-version: '3.12'
28
+ python-version: '3.13'
29
29
  - name: Install dependencies
30
30
  run: |
31
31
  python -m pip install --upgrade pip
@@ -18,7 +18,7 @@ jobs:
18
18
  - name: 🏗 Set up Python
19
19
  uses: actions/setup-python@v5.0.0
20
20
  with:
21
- python-version: "3.12"
21
+ python-version: "3.13"
22
22
  cache: "pip"
23
23
  - name: 🏗 Install requirements
24
24
  run: python3 -m pip install -r requirements.txt
@@ -9,7 +9,7 @@ on:
9
9
  workflow_dispatch:
10
10
 
11
11
  env:
12
- DEFAULT_PYTHON: "3.12"
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.12"]
20
+ python: ["3.13"]
21
21
  steps:
22
22
  - name: ⤵️ Check out code from GitHub
23
23
  uses: actions/checkout@v4.1.7
@@ -10,7 +10,7 @@ on:
10
10
  - main
11
11
 
12
12
  env:
13
- DEFAULT_PYTHON: "3.12"
13
+ DEFAULT_PYTHON: "3.13"
14
14
 
15
15
  jobs:
16
16
  mypy:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solarlog_cli
3
- Version: 0.6.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "solarlog_cli"
3
- version = "0.6.1"
3
+ version = "0.7.1"
4
4
  authors = [
5
5
  { name="dontinelli", email="73341522+dontinelli@users.noreply.github.com" },
6
6
  ]
@@ -1,4 +1,4 @@
1
- aiohttp>=3.9.5
1
+ aiohttp>=3.13.3
2
2
  aioresponses == 0.7.6
3
3
  bcrypt >= 4.3.0
4
4
  mashumaro>=3.13
@@ -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({"SolarLog": response.cookies["SolarLog"].value})
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", response.cookies["SolarLog"].value)
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, data: SolarlogData) -> SolarlogData:
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
- data.production_year = raw_data["878"][-1][1]
262
- data.self_consumption_year = raw_data["878"][-1][3]
270
+ return EnergyData(
271
+ production=raw_data["878"][-1][1],
272
+ self_consumption=raw_data["878"][-1][3]
273
+ )
263
274
 
264
- return data
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 update_device_list(self) -> dict[int, InverterData]:
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
@@ -87,3 +87,9 @@
87
87
  }),
88
88
  })
89
89
  # ---
90
+ # name: test_update_energy_data[asyncio]
91
+ dict({
92
+ 'production': 8852462,
93
+ 'self_consumption': 2071,
94
+ })
95
+ # ---
@@ -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