solarlog-cli 0.5.0__tar.gz → 0.6.0__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.5.0 → solarlog_cli-0.6.0}/.coverage +0 -0
  2. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/PKG-INFO +2 -1
  3. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/pyproject.toml +2 -1
  4. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/requirements.txt +1 -0
  5. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/src/solarlog_cli/solarlog_client.py +34 -4
  6. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/src/solarlog_cli/solarlog_connector.py +7 -2
  7. solarlog_cli-0.6.0/tests/fixtures/user_salts.json +16 -0
  8. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/test_solarlog_cli.py +75 -7
  9. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.devcontainer/devcontainer.json +0 -0
  10. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.github/workflows/pylint.yml +0 -0
  11. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.github/workflows/python-publish.yml +0 -0
  12. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.github/workflows/ruff.yml +0 -0
  13. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.github/workflows/tests.yml +0 -0
  14. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.github/workflows/typing.yml +0 -0
  15. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.gitignore +0 -0
  16. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.vscode/settings.json +0 -0
  17. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.vscode/tasks.json +0 -0
  18. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/LICENSE +0 -0
  19. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/README.md +0 -0
  20. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/pytest.ini +0 -0
  21. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/src/solarlog_cli/__init__.py +0 -0
  22. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/src/solarlog_cli/py.typed +0 -0
  23. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/src/solarlog_cli/solarlog_exceptions.py +0 -0
  24. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/src/solarlog_cli/solarlog_models.py +0 -0
  25. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/__init__.py +0 -0
  26. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/__snapshots__/test_solarlog_cli.ambr +0 -0
  27. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/conftest.py +0 -0
  28. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/basic_data.json +0 -0
  29. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/basic_data_during_update.json +0 -0
  30. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/basic_data_no_power.json +0 -0
  31. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/battery_data.json +0 -0
  32. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/battery_data_without_battery.json +0 -0
  33. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/device_data_1.json +0 -0
  34. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/device_data_2.json +0 -0
  35. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/device_data_3.json +0 -0
  36. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/device_data_4.json +0 -0
  37. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/device_list.json +0 -0
  38. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/device_list_access_denied.json +0 -0
  39. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/energy_per_inverter.json +0 -0
  40. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/extended_data.json +0 -0
  41. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/power_per_inverter.json +0 -0
  42. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/ruff.toml +0 -0
  43. {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/syrupy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solarlog_cli
3
- Version: 0.5.0
3
+ Version: 0.6.0
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
@@ -15,6 +15,7 @@ Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Requires-Python: >=3.12
17
17
  Requires-Dist: aiohttp
18
+ Requires-Dist: bcrypt>=4.3.0
18
19
  Requires-Dist: mashumaro>=3.13
19
20
  Description-Content-Type: text/markdown
20
21
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "solarlog_cli"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  authors = [
5
5
  { name="dontinelli", email="73341522+dontinelli@users.noreply.github.com" },
6
6
  ]
@@ -9,6 +9,7 @@ readme = "README.md"
9
9
  requires-python = ">=3.12"
10
10
  dependencies = [
11
11
  "aiohttp",
12
+ "bcrypt>=4.3.0",
12
13
  "mashumaro>=3.13",
13
14
  ]
14
15
  classifiers = [
@@ -1,5 +1,6 @@
1
1
  aiohttp>=3.9.5
2
2
  aioresponses == 0.7.6
3
+ bcrypt >= 4.3.0
3
4
  mashumaro>=3.13
4
5
  pytest-aio==1.9.0
5
6
  pytest-cov==5.0.0
@@ -9,6 +9,7 @@ import logging
9
9
  from typing import Any
10
10
 
11
11
  from aiohttp import ClientResponse, ClientSession, ClientTimeout
12
+ import bcrypt
12
13
 
13
14
  from .solarlog_exceptions import (
14
15
  SolarLogAuthenticationError,
@@ -70,13 +71,42 @@ class Client:
70
71
  response = await self.execute_http_request(payload,"login")
71
72
 
72
73
  text = await response.text()
73
- if text.count("FAILED - Password was wrong"):
74
- raise SolarLogAuthenticationError
74
+ _LOGGER.debug("Response: %s",text)
75
75
  if text.count("FAILED - User was wrong"):
76
76
  #Response means, that no password is required
77
77
  self.password = ""
78
78
  return False
79
79
 
80
+ if text.count("FAILED - Password was wrong"):
81
+ # For newer firmware, login with encrypted PWD is required.
82
+ # Therefore test with encrypted PWD and only raise authentication error,
83
+ # if login with encrypted PWD fails as well.
84
+
85
+ payload = '{ "550": None }'
86
+
87
+ response = await self.execute_http_request(payload)
88
+
89
+ text = await response.text()
90
+ _LOGGER.debug("Response to request for user salts: %s",text)
91
+ r_dict: dict[str, Any] = json.loads(text)
92
+
93
+ salt: str = r_dict.get('550',{}).get('104')
94
+ _LOGGER.debug("Salt to hash pwd: %s",salt)
95
+
96
+ try:
97
+ if salt != 'QUERY IMPOSSIBLE 000' and salt is not None:
98
+ hashed_password = bcrypt.hashpw(self.password.encode(), salt.encode())
99
+ payload = f"u=user&p={hashed_password.decode('utf-8')}"
100
+ response = await self.execute_http_request(payload,"login")
101
+ text = await response.text()
102
+ _LOGGER.debug("Response of login with hashed pwd: %s",text)
103
+ if text.count("FAILED - Password was wrong"):
104
+ _LOGGER.debug("Wrong password (hashed)")
105
+ raise SolarLogAuthenticationError
106
+ self.password = hashed_password.decode('utf-8')
107
+ except Exception as exception:
108
+ raise SolarLogAuthenticationError from exception
109
+
80
110
  self.token = response.cookies["SolarLog"].value
81
111
 
82
112
  _LOGGER.debug("response: %s",text)
@@ -93,7 +123,7 @@ class Client:
93
123
 
94
124
  url = f"{self.host}/{path}"
95
125
 
96
- header = {"Content-Type": "text/html"}
126
+ header = {"Content-Type": "text/html", "X-SL-CSRF-PROTECTION": "1"}
97
127
  if self.token != "":
98
128
  header |= {"Cookie": f"SolarLog={self.token}"}
99
129
  body = f"token={self.token}; " + body
@@ -135,7 +165,7 @@ class Client:
135
165
  if text.count('{"QUERY IMPOSSIBLE 000"}'):
136
166
  raise SolarLogUpdateError(f"Server response: {text}")
137
167
 
138
- if text.count("ACCESS DENIED"):
168
+ if text.count("ACCESS DENIED") and not text.startswith('{"550":{'):
139
169
  raise SolarLogAuthenticationError(f"Server response: {text}")
140
170
 
141
171
  try:
@@ -63,8 +63,13 @@ class SolarLogConnector:
63
63
  return False
64
64
  except SolarLogAuthenticationError as err:
65
65
  #User has no unprotected access to extended API, try to log in
66
- _LOGGER.debug("Authentication error: %s", err)
67
- self.extended_data = await self.login()
66
+ #(login returns false, if no pwd is set)
67
+ _LOGGER.debug("Auth error during test for extended data: %s", err)
68
+ try:
69
+ self.extended_data = await self.login()
70
+ except SolarLogAuthenticationError as error:
71
+ _LOGGER.debug("Auth error during login in test for extended data: %s", err)
72
+ raise SolarLogAuthenticationError from error
68
73
  _LOGGER.debug("Login successful?: %s", self.extended_data)
69
74
  else:
70
75
  self.extended_data = True
@@ -0,0 +1,16 @@
1
+ {
2
+ "550": {
3
+ "100":"ACCESS DENIED",
4
+ "101":"ACCESS DENIED",
5
+ "102":"ACCESS DENIED",
6
+ "103":0,
7
+ "104":"$2b$08$yw.OTuzhjCHukKjYO0diL.",
8
+ "105":"ACCESS DENIED",
9
+ "106":1,
10
+ "107":"SALT2",
11
+ "108":"ACCESS DENIED",
12
+ "109":0,"110":"SALT3",
13
+ "111":"ACCESS DENIED",
14
+ "112":1
15
+ }
16
+ }
@@ -58,7 +58,8 @@ async def test_existing_session(
58
58
  status=200,
59
59
  )
60
60
 
61
- solarlog_connector = SolarLogConnector("localhost", session=ClientSession())
61
+ solarlog_connector = SolarLogConnector(
62
+ "localhost", session=ClientSession())
62
63
 
63
64
  assert await solarlog_connector.test_connection()
64
65
 
@@ -69,12 +70,14 @@ async def test_existing_session(
69
70
  await solarlog_connector.client.close()
70
71
  assert solarlog_connector.client.session.closed
71
72
 
73
+
72
74
  async def test_extended_data_available(
73
75
  responses: aioresponses,
74
76
  ) -> None:
75
77
  """Test extended data available."""
76
78
 
77
- solarlog_connector = SolarLogConnector("http://solarlog.com", password="pwd")
79
+ solarlog_connector = SolarLogConnector(
80
+ "http://solarlog.com", password="pwd")
78
81
 
79
82
  responses.post(
80
83
  "http://solarlog.com/getjp",
@@ -99,6 +102,27 @@ async def test_extended_data_available(
99
102
  assert not await solarlog_connector.test_extended_data_available()
100
103
  assert solarlog_connector.password == ""
101
104
 
105
+ responses.post(
106
+ "http://solarlog.com/getjp",
107
+ body=load_fixture("device_list_access_denied.json"),
108
+ )
109
+ responses.post(
110
+ "http://solarlog.com/login",
111
+ body="FAILED - Password was wrong",
112
+ )
113
+ responses.post(
114
+ "http://solarlog.com/getjp",
115
+ body=load_fixture("user_salts.json"),
116
+ )
117
+ responses.post(
118
+ "http://solarlog.com/login",
119
+ exception=SolarLogAuthenticationError('test'),
120
+ )
121
+ solarlog_connector.client.password = "pwd"
122
+ # type: ignore [call-overload]
123
+ with pytest.raises(SolarLogAuthenticationError):
124
+ await solarlog_connector.test_extended_data_available()
125
+
102
126
  responses.post(
103
127
  "http://solarlog.com/getjp",
104
128
  body=load_fixture("device_list.json"),
@@ -108,6 +132,7 @@ async def test_extended_data_available(
108
132
  await solarlog_connector.client.close()
109
133
  assert solarlog_connector.client.session.closed
110
134
 
135
+
111
136
  async def test_login_and_data_retreival(responses: aioresponses) -> None:
112
137
  """Test login into Solar-Log."""
113
138
  responses.post(
@@ -115,7 +140,8 @@ async def test_login_and_data_retreival(responses: aioresponses) -> None:
115
140
  headers={"Set-Cookie": "SolarLog=token"},
116
141
  body="SUCCESS - Password was correct, you are now logged in",
117
142
  )
118
- solarlog_connector = SolarLogConnector("http://solarlog.com", password="pwd")
143
+ solarlog_connector = SolarLogConnector(
144
+ "http://solarlog.com", password="pwd")
119
145
 
120
146
  assert await solarlog_connector.login()
121
147
  assert solarlog_connector.client.token == "token"
@@ -129,15 +155,52 @@ async def test_login_and_data_retreival(responses: aioresponses) -> None:
129
155
  await solarlog_connector.client.close()
130
156
  assert solarlog_connector.client.session.closed
131
157
 
158
+
159
+ async def test_login_hashed_pwd(responses: aioresponses) -> None:
160
+ """Test login into Solar-Log."""
161
+
162
+ responses.post(
163
+ "http://solarlog.com/login",
164
+ body="FAILED - Password was wrong",
165
+ )
166
+ responses.post(
167
+ "http://solarlog.com/getjp",
168
+ body=load_fixture("user_salts.json"),
169
+ )
170
+ responses.post(
171
+ "http://solarlog.com/login",
172
+ headers={"Set-Cookie": "SolarLog=token"},
173
+ body="SUCCESS - Password was correct, you are now logged in",
174
+ )
175
+ solarlog_connector = SolarLogConnector(
176
+ "http://solarlog.com", password="pwd")
177
+
178
+ assert await solarlog_connector.login()
179
+ assert solarlog_connector.client.token == "token"
180
+
181
+ await solarlog_connector.client.close()
182
+ assert solarlog_connector.client.session.closed
183
+
184
+
132
185
  async def test_login_exceptions(responses: aioresponses) -> None:
133
186
  """Test exceptions at login into Solar-Log."""
134
- solarlog_connector = SolarLogConnector("http://solarlog.com", password="pwd")
187
+ solarlog_connector = SolarLogConnector(
188
+ "http://solarlog.com", password="pwd")
135
189
 
136
190
  responses.post(
137
191
  "http://solarlog.com/login",
138
192
  body="FAILED - Password was wrong",
139
193
  )
140
- with pytest.raises(SolarLogAuthenticationError): # type: ignore [call-overload]
194
+ responses.post(
195
+ "http://solarlog.com/getjp",
196
+ body=load_fixture("user_salts.json"),
197
+ )
198
+ responses.post(
199
+ "http://solarlog.com/login",
200
+ body="FAILED - Password was wrong",
201
+ )
202
+ # type: ignore [call-overload]
203
+ with pytest.raises(SolarLogAuthenticationError):
141
204
  await solarlog_connector.client.login()
142
205
 
143
206
  responses.post(
@@ -155,6 +218,7 @@ async def test_login_exceptions(responses: aioresponses) -> None:
155
218
  await solarlog_connector.client.close()
156
219
  assert solarlog_connector.client.session.closed
157
220
 
221
+
158
222
  async def test_update_data(
159
223
  responses: aioresponses,
160
224
  snapshot: SnapshotAssertion
@@ -195,6 +259,7 @@ async def test_update_data(
195
259
  await solarlog_connector.client.close()
196
260
  assert solarlog_connector.client.session.closed
197
261
 
262
+
198
263
  async def test_update_data_without_battery(
199
264
  responses: aioresponses,
200
265
  ) -> None:
@@ -234,6 +299,7 @@ async def test_update_data_without_battery(
234
299
  await solarlog_connector.client.close()
235
300
  assert solarlog_connector.client.session.closed
236
301
 
302
+
237
303
  @pytest.mark.parametrize(
238
304
  ("status", "request_timeout", "error"),
239
305
  [
@@ -364,11 +430,13 @@ async def test_enabled_devices(responses: aioresponses) -> None:
364
430
  body=load_fixture("device_list.json"),
365
431
  )
366
432
 
367
- solarlog_connector = SolarLogConnector("http://solarlog.com",device_enabled={})
433
+ solarlog_connector = SolarLogConnector(
434
+ "http://solarlog.com", device_enabled={})
368
435
  assert solarlog_connector.device_enabled() == {}
369
436
  assert await solarlog_connector.update_device_list() == {}
370
437
 
371
- solarlog_connector.set_enabled_devices({0: True, 1: False, 2: False, 3: True})
438
+ solarlog_connector.set_enabled_devices(
439
+ {0: True, 1: False, 2: False, 3: True})
372
440
 
373
441
  assert solarlog_connector.device_enabled(0)
374
442
  assert not solarlog_connector.device_enabled(1)
File without changes
File without changes
File without changes
File without changes