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.
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.coverage +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/PKG-INFO +2 -1
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/pyproject.toml +2 -1
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/requirements.txt +1 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/src/solarlog_cli/solarlog_client.py +34 -4
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/src/solarlog_cli/solarlog_connector.py +7 -2
- solarlog_cli-0.6.0/tests/fixtures/user_salts.json +16 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/test_solarlog_cli.py +75 -7
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.devcontainer/devcontainer.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.github/workflows/pylint.yml +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.github/workflows/python-publish.yml +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.github/workflows/ruff.yml +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.github/workflows/tests.yml +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.github/workflows/typing.yml +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.gitignore +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.vscode/settings.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/.vscode/tasks.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/LICENSE +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/README.md +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/pytest.ini +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/src/solarlog_cli/__init__.py +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/src/solarlog_cli/py.typed +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/src/solarlog_cli/solarlog_exceptions.py +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/src/solarlog_cli/solarlog_models.py +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/__init__.py +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/__snapshots__/test_solarlog_cli.ambr +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/conftest.py +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/basic_data.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/basic_data_during_update.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/basic_data_no_power.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/battery_data.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/battery_data_without_battery.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/device_data_1.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/device_data_2.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/device_data_3.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/device_data_4.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/device_list.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/device_list_access_denied.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/energy_per_inverter.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/extended_data.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/fixtures/power_per_inverter.json +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/ruff.toml +0 -0
- {solarlog_cli-0.5.0 → solarlog_cli-0.6.0}/tests/syrupy.py +0 -0
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solarlog_cli
|
|
3
|
-
Version: 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.
|
|
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 = [
|
|
@@ -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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|