smartbox 2.1.2__tar.gz → 2.1.3__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.
- {smartbox-2.1.2 → smartbox-2.1.3}/PKG-INFO +1 -1
- {smartbox-2.1.2 → smartbox-2.1.3}/pyproject.toml +1 -1
- {smartbox-2.1.2 → smartbox-2.1.3}/src/smartbox/cmd.py +24 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/src/smartbox/models.py +37 -4
- {smartbox-2.1.2 → smartbox-2.1.3}/src/smartbox/resailer.py +6 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/src/smartbox/session.py +45 -13
- {smartbox-2.1.2 → smartbox-2.1.3}/src/smartbox/socket.py +8 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/src/smartbox/update_manager.py +19 -0
- smartbox-2.1.3/tests/fixtures/devs/device1/connected.json +3 -0
- smartbox-2.1.3/tests/fixtures/devs/device1/mgr/nodes.json +31 -0
- smartbox-2.1.3/tests/fixtures/devs/device1/pmo/3/samples.json +16 -0
- smartbox-2.1.3/tests/fixtures/devs/device1/pmo/3/setup.json +5 -0
- smartbox-2.1.3/tests/fixtures/devs/device1/pmo/3/status.json +4 -0
- smartbox-2.1.3/tests/fixtures/devs/device2/connected.json +3 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/test_cmd.py +26 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/test_resailer.py +13 -1
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/test_session.py +87 -10
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/test_update_manager.py +18 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/uv.lock +2 -1
- smartbox-2.1.2/requirements.txt +0 -2
- smartbox-2.1.2/tests/fixtures/devs/device1/mgr/nodes.json +0 -18
- {smartbox-2.1.2 → smartbox-2.1.3}/.github/FUNDING.yml +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/.github/ISSUE_TEMPLATE/new-resailer.md +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/.github/dependabot.yml +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/.github/workflows/publish-to-pypi.yml +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/.github/workflows/python-package.yml +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/.gitignore +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/.pre-commit-config.yaml +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/.vscode/settings.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/CHANGELOG.md +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/CODEOWNERS +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/LICENSE +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/README.md +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/api-notes.md +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/codecov.yml +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/src/smartbox/__init__.py +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/src/smartbox/error.py +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/src/smartbox/py.typed +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/__init__.py +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/common.py +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/conftest.py +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device1/acm/1/samples.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device1/acm/1/setup.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device1/acm/1/status.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device1/htr/0/samples.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device1/htr/0/setup.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device1/htr/0/status.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device1/htr_system/power_limit.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device1/mgr/away_status.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/0/samples.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/0/setup.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/0/status.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/1/samples.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/1/setup.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/1/status.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/2/samples.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/2/setup.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/2/status.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/3/samples.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/3/setup.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/3/status.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/4/samples.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/4/setup.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/4/status.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/5/samples.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/5/setup.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_mod/5/status.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/htr_system/power_limit.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/mgr/away_status.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs/device2/mgr/nodes.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/devs.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/grouped_devs.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/groups/test_home/guest_users.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/fixtures/test_path.json +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tests/test_models.py +0 -0
- {smartbox-2.1.2 → smartbox-2.1.3}/tox.ini +0 -0
|
@@ -277,6 +277,21 @@ async def device_away_status(ctx) -> None:
|
|
|
277
277
|
_pretty_print(device_away_status)
|
|
278
278
|
|
|
279
279
|
|
|
280
|
+
@smartbox.command(help="Show device connected status")
|
|
281
|
+
@click.pass_context
|
|
282
|
+
async def device_connected_status(ctx) -> None:
|
|
283
|
+
"""Show device connected status."""
|
|
284
|
+
session = ctx.obj["session"]
|
|
285
|
+
devices = await session.get_devices()
|
|
286
|
+
|
|
287
|
+
for device in devices:
|
|
288
|
+
print(f"{device['name']} (dev_id: {device['dev_id']})")
|
|
289
|
+
device_away_status = await session.get_device_connected(
|
|
290
|
+
device["dev_id"],
|
|
291
|
+
)
|
|
292
|
+
_pretty_print(device_away_status)
|
|
293
|
+
|
|
294
|
+
|
|
280
295
|
@smartbox.command(
|
|
281
296
|
help="Set device away_status (pass settings as extra args, e.g. away=true)",
|
|
282
297
|
)
|
|
@@ -379,6 +394,15 @@ async def health_check(ctx) -> None:
|
|
|
379
394
|
_pretty_print(health)
|
|
380
395
|
|
|
381
396
|
|
|
397
|
+
@smartbox.command(help="Get version of the API.")
|
|
398
|
+
@click.pass_context
|
|
399
|
+
async def api_version(ctx) -> None:
|
|
400
|
+
"""Get the version of the API."""
|
|
401
|
+
session = ctx.obj["session"]
|
|
402
|
+
version = await session.api_version()
|
|
403
|
+
_pretty_print(version)
|
|
404
|
+
|
|
405
|
+
|
|
382
406
|
@smartbox.command(help="Get the availables resailers.")
|
|
383
407
|
def resailers() -> None:
|
|
384
408
|
"""Get the availables resailers."""
|
|
@@ -47,7 +47,15 @@ class NodeExtraOptions(BaseModel):
|
|
|
47
47
|
boost_time: int
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
class
|
|
50
|
+
class PmoSetup(BaseModel):
|
|
51
|
+
"""Pmo node setup."""
|
|
52
|
+
|
|
53
|
+
circuit_type: int
|
|
54
|
+
power_limit: int
|
|
55
|
+
reverse: bool
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DefaultNodeSetup(BaseModel):
|
|
51
59
|
"""NodeSetup model."""
|
|
52
60
|
|
|
53
61
|
sync_status: str
|
|
@@ -66,6 +74,16 @@ class NodeSetup(BaseModel):
|
|
|
66
74
|
extra_options: NodeExtraOptions
|
|
67
75
|
|
|
68
76
|
|
|
77
|
+
class NodeSetup(RootModel[DefaultNodeSetup | PmoSetup]):
|
|
78
|
+
"""NodeSetup model."""
|
|
79
|
+
|
|
80
|
+
root: DefaultNodeSetup | PmoSetup
|
|
81
|
+
|
|
82
|
+
def __getattr__(self, name: str) -> DefaultNodeSetup | PmoSetup:
|
|
83
|
+
"""Get the root model directly."""
|
|
84
|
+
return getattr(self.root, name)
|
|
85
|
+
|
|
86
|
+
|
|
69
87
|
class DefaultNodeStatus(BaseModel):
|
|
70
88
|
"""Default Node Status."""
|
|
71
89
|
|
|
@@ -196,17 +214,26 @@ class Homes(RootModel[list[Home]]):
|
|
|
196
214
|
|
|
197
215
|
|
|
198
216
|
class Sample(BaseModel):
|
|
199
|
-
"""Sample model."""
|
|
217
|
+
"""Pmo Sample model."""
|
|
200
218
|
|
|
201
219
|
t: int
|
|
220
|
+
counter: float
|
|
202
221
|
temp: str
|
|
203
|
-
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class PmoSample(BaseModel):
|
|
225
|
+
"""Default Sample."""
|
|
226
|
+
|
|
227
|
+
t: int
|
|
228
|
+
counter: float
|
|
229
|
+
max: int
|
|
230
|
+
min: int
|
|
204
231
|
|
|
205
232
|
|
|
206
233
|
class Samples(BaseModel):
|
|
207
234
|
"""Samples model."""
|
|
208
235
|
|
|
209
|
-
samples: list[Sample]
|
|
236
|
+
samples: list[PmoSample | Sample]
|
|
210
237
|
|
|
211
238
|
|
|
212
239
|
class Token(BaseModel):
|
|
@@ -229,3 +256,9 @@ class Guests(BaseModel):
|
|
|
229
256
|
"""Guests model."""
|
|
230
257
|
|
|
231
258
|
guest_users: list[GuestUser]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class DeviceConnected(BaseModel):
|
|
262
|
+
"""Connected status of devices."""
|
|
263
|
+
|
|
264
|
+
connected: bool
|
|
@@ -31,6 +31,12 @@ class AvailableResailers:
|
|
|
31
31
|
api_url="api",
|
|
32
32
|
serial_id=1,
|
|
33
33
|
),
|
|
34
|
+
"api-ehc": SmartboxResailer(
|
|
35
|
+
name="Electric Heating Company",
|
|
36
|
+
web_url="https://control.electric-heatingcompany.co.uk/",
|
|
37
|
+
api_url="api-ehc",
|
|
38
|
+
serial_id=3,
|
|
39
|
+
),
|
|
34
40
|
"api-climastar": SmartboxResailer(
|
|
35
41
|
name="Climastar",
|
|
36
42
|
web_url="https://avantwifi.climastar.es/",
|
|
@@ -16,6 +16,7 @@ from smartbox.models import (
|
|
|
16
16
|
AcmNodeStatus,
|
|
17
17
|
DefaultNodeStatus,
|
|
18
18
|
DeviceAwayStatus,
|
|
19
|
+
DeviceConnected,
|
|
19
20
|
Devices,
|
|
20
21
|
Guests,
|
|
21
22
|
Home,
|
|
@@ -131,6 +132,18 @@ class AsyncSession:
|
|
|
131
132
|
raise APIUnavailableError(e) from e
|
|
132
133
|
return await response.json()
|
|
133
134
|
|
|
135
|
+
async def api_version(self) -> dict[str, str]:
|
|
136
|
+
"""Check if the API is alived."""
|
|
137
|
+
api_url = f"{self._api_host}/version"
|
|
138
|
+
try:
|
|
139
|
+
response = await self.client.get(api_url)
|
|
140
|
+
except (
|
|
141
|
+
aiohttp.ClientConnectionError,
|
|
142
|
+
aiohttp.ClientConnectorError,
|
|
143
|
+
) as e:
|
|
144
|
+
raise APIUnavailableError(e) from e
|
|
145
|
+
return await response.json()
|
|
146
|
+
|
|
134
147
|
async def _authentication(self, credentials: dict[str, str]) -> None:
|
|
135
148
|
"""Do the authentication process to Smartbox. First one use login/mdp/basic_auth. Then the tokens."""
|
|
136
149
|
token_headers = self._headers.copy()
|
|
@@ -209,6 +222,7 @@ class AsyncSession:
|
|
|
209
222
|
try:
|
|
210
223
|
_LOGGER.debug("Getting %s.", api_url)
|
|
211
224
|
response = await self.client.get(api_url, headers=self._headers)
|
|
225
|
+
_LOGGER.debug("Response %s.", (await response.json()))
|
|
212
226
|
except (
|
|
213
227
|
aiohttp.ClientConnectionError,
|
|
214
228
|
aiohttp.ClientConnectorError,
|
|
@@ -239,6 +253,7 @@ class AsyncSession:
|
|
|
239
253
|
data=data_str,
|
|
240
254
|
headers=self._headers,
|
|
241
255
|
)
|
|
256
|
+
_LOGGER.debug("Response %s.", (await response.json()))
|
|
242
257
|
except (
|
|
243
258
|
aiohttp.ClientConnectionError,
|
|
244
259
|
aiohttp.ClientConnectorError,
|
|
@@ -306,6 +321,17 @@ class AsyncSmartboxSession(AsyncSession):
|
|
|
306
321
|
return response["nodes"]
|
|
307
322
|
return Nodes.model_validate(response).nodes
|
|
308
323
|
|
|
324
|
+
async def get_device_connected(
|
|
325
|
+
self,
|
|
326
|
+
device_id: str,
|
|
327
|
+
) -> dict[str, bool] | DeviceConnected:
|
|
328
|
+
"""Get device away status."""
|
|
329
|
+
response = await self._api_request(f"devs/{device_id}/connected")
|
|
330
|
+
status: DeviceConnected = DeviceConnected.model_validate(response)
|
|
331
|
+
if self.raw_response is False:
|
|
332
|
+
return status
|
|
333
|
+
return status.model_dump(mode="json")
|
|
334
|
+
|
|
309
335
|
async def get_device_away_status(
|
|
310
336
|
self,
|
|
311
337
|
device_id: str,
|
|
@@ -333,15 +359,18 @@ class AsyncSmartboxSession(AsyncSession):
|
|
|
333
359
|
self, device_id: str, node: dict[str, Any] | None = None
|
|
334
360
|
) -> int:
|
|
335
361
|
"""Get device power limit."""
|
|
336
|
-
|
|
362
|
+
power_param = "power_limit"
|
|
363
|
+
url = f"devs/{device_id}/htr_system/{power_param}"
|
|
364
|
+
|
|
337
365
|
if node is not None and (
|
|
338
366
|
(_node := Node.model_validate(node))
|
|
339
367
|
and _node.type == SmartboxNodeType.PMO
|
|
340
368
|
):
|
|
341
|
-
|
|
369
|
+
power_param = "power"
|
|
370
|
+
url = f"devs/{device_id}/{_node.type}/{_node.addr}/{power_param}"
|
|
342
371
|
|
|
343
372
|
resp = await self._api_request(url)
|
|
344
|
-
return int(resp[
|
|
373
|
+
return int(resp[power_param])
|
|
345
374
|
|
|
346
375
|
async def set_device_power_limit(
|
|
347
376
|
self,
|
|
@@ -380,14 +409,13 @@ class AsyncSmartboxSession(AsyncSession):
|
|
|
380
409
|
datetime.datetime.fromtimestamp(end_time, tz=datetime.UTC),
|
|
381
410
|
)
|
|
382
411
|
_node: Node = Node.model_validate(node)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
f"devs/{device_id}/{_node.type}/{_node.addr}/samples?start={start_time}&end={end_time}",
|
|
386
|
-
),
|
|
412
|
+
response = await self._api_request(
|
|
413
|
+
f"devs/{device_id}/{_node.type}/{_node.addr}/samples?start={start_time}&end={end_time}",
|
|
387
414
|
)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
415
|
+
_LOGGER.debug("Get_Device_Samples_Node: %s", response)
|
|
416
|
+
if self.raw_response is True:
|
|
417
|
+
return response
|
|
418
|
+
return Samples.model_validate(response)
|
|
391
419
|
|
|
392
420
|
async def get_node_status(
|
|
393
421
|
self,
|
|
@@ -399,6 +427,7 @@ class AsyncSmartboxSession(AsyncSession):
|
|
|
399
427
|
| HtrNodeStatus
|
|
400
428
|
| HtrModNodeStatus
|
|
401
429
|
| DefaultNodeStatus
|
|
430
|
+
| None
|
|
402
431
|
):
|
|
403
432
|
"""Get a node status."""
|
|
404
433
|
_node: Node = Node.model_validate(node)
|
|
@@ -461,9 +490,12 @@ class AsyncSmartboxSession(AsyncSession):
|
|
|
461
490
|
data = {k: v for k, v in setup_args.items() if v is not None}
|
|
462
491
|
# setup seems to require all settings to be re-posted, so get current
|
|
463
492
|
# values and update
|
|
464
|
-
setup_data
|
|
465
|
-
|
|
466
|
-
|
|
493
|
+
setup_data: dict[str, Any] = {}
|
|
494
|
+
node_setup = await self.get_node_setup(device_id, node)
|
|
495
|
+
if not isinstance(node_setup, dict):
|
|
496
|
+
setup_data = node_setup.model_dump(mode="json")
|
|
497
|
+
else:
|
|
498
|
+
setup_data = node_setup
|
|
467
499
|
setup_data.update(data)
|
|
468
500
|
await self._api_post(
|
|
469
501
|
data=setup_data,
|
|
@@ -143,6 +143,13 @@ class SocketSession:
|
|
|
143
143
|
|
|
144
144
|
event_loop.add_signal_handler(signal.SIGINT, sigint_handler)
|
|
145
145
|
|
|
146
|
+
async def _dev_data(self) -> None:
|
|
147
|
+
"""Send first dev data."""
|
|
148
|
+
if not self._api_v2_ns.connected:
|
|
149
|
+
_LOGGER.debug("Namespace disconnected, not sending ping")
|
|
150
|
+
_LOGGER.debug("Sending dev_data event")
|
|
151
|
+
await self._sio.emit("dev_data", namespace=_API_V2_NAMESPACE)
|
|
152
|
+
|
|
146
153
|
async def _send_ping(self) -> None:
|
|
147
154
|
"""End pings to be alive."""
|
|
148
155
|
_LOGGER.debug("Starting ping task every %ss", self._ping_interval)
|
|
@@ -196,6 +203,7 @@ class SocketSession:
|
|
|
196
203
|
)
|
|
197
204
|
else:
|
|
198
205
|
_LOGGER.info("Successfully connected to %s", url)
|
|
206
|
+
await self._dev_data()
|
|
199
207
|
await self._sio.wait()
|
|
200
208
|
_LOGGER.info("Socket loop exited, disconnecting")
|
|
201
209
|
await self._sio.disconnect()
|
|
@@ -133,6 +133,10 @@ class UpdateManager:
|
|
|
133
133
|
"""Run the socket session asynchronously, waiting for updates."""
|
|
134
134
|
await self._socket_session.run()
|
|
135
135
|
|
|
136
|
+
async def cancel(self) -> None:
|
|
137
|
+
"""Disconnecting and cancelling tasks."""
|
|
138
|
+
await self._socket_session.cancel()
|
|
139
|
+
|
|
136
140
|
def subscribe_to_dev_data(self, jq_expr: str, callback: Callable) -> None:
|
|
137
141
|
"""Subscribe to receive device data."""
|
|
138
142
|
sub = DevDataSubscription(jq_expr, callback)
|
|
@@ -163,6 +167,21 @@ class UpdateManager:
|
|
|
163
167
|
callback,
|
|
164
168
|
)
|
|
165
169
|
|
|
170
|
+
def subscribe_to_device_connected(
|
|
171
|
+
self,
|
|
172
|
+
callback: Callable[[bool], None],
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Subscribe to device power limit updates."""
|
|
175
|
+
self.subscribe_to_dev_data(
|
|
176
|
+
".connected",
|
|
177
|
+
lambda p: callback(bool(p)),
|
|
178
|
+
)
|
|
179
|
+
self.subscribe_to_updates(
|
|
180
|
+
r"^/connected",
|
|
181
|
+
f"{self.BODY_PATH}.connected",
|
|
182
|
+
lambda p: callback(bool(p)),
|
|
183
|
+
)
|
|
184
|
+
|
|
166
185
|
def subscribe_to_device_power_limit(
|
|
167
186
|
self,
|
|
168
187
|
callback: Callable[[int], None],
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"nodes": [
|
|
3
|
+
{
|
|
4
|
+
"addr": 0,
|
|
5
|
+
"installed": true,
|
|
6
|
+
"lost": false,
|
|
7
|
+
"name": "Node 1 0",
|
|
8
|
+
"type": "htr"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"addr": 1,
|
|
12
|
+
"installed": true,
|
|
13
|
+
"lost": false,
|
|
14
|
+
"name": "Node 1 1",
|
|
15
|
+
"type": "acm"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"addr": 3,
|
|
19
|
+
"installed": true,
|
|
20
|
+
"level": 1,
|
|
21
|
+
"lost": false,
|
|
22
|
+
"name": "Node 1 3",
|
|
23
|
+
"parent": 1,
|
|
24
|
+
"setup": {
|
|
25
|
+
"counter_offset": "679.33"
|
|
26
|
+
},
|
|
27
|
+
"type": "pmo",
|
|
28
|
+
"uid": "003E003F0D47373436363733"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -28,6 +28,22 @@ async def test_health_check(runner, mock_session):
|
|
|
28
28
|
assert "" in result.output
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
@pytest.mark.anyio
|
|
32
|
+
async def test_api_version(runner, mock_session):
|
|
33
|
+
devices_future = asyncio.Future()
|
|
34
|
+
devices_future.set_result(
|
|
35
|
+
{"major": "1", "minor": "53", "subminor": "2", "commit": "NULL"}
|
|
36
|
+
)
|
|
37
|
+
mock_session.return_value.api_version.return_value = devices_future
|
|
38
|
+
|
|
39
|
+
result = await runner.invoke(
|
|
40
|
+
smartbox,
|
|
41
|
+
[*DEFAULT_ARGS, "api-version"],
|
|
42
|
+
)
|
|
43
|
+
assert result.exit_code == 0
|
|
44
|
+
assert "subminor" in result.output
|
|
45
|
+
|
|
46
|
+
|
|
31
47
|
@pytest.mark.anyio
|
|
32
48
|
async def test_devices(runner, async_smartbox_session):
|
|
33
49
|
result = await runner.invoke(
|
|
@@ -365,3 +381,13 @@ async def test_guests(runner, mock_session):
|
|
|
365
381
|
mock_session.return_value.get_home_guests.assert_called_once_with(
|
|
366
382
|
home_id="home1"
|
|
367
383
|
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@pytest.mark.anyio
|
|
387
|
+
async def test_device_connected_status(runner, async_smartbox_session):
|
|
388
|
+
result = await runner.invoke(
|
|
389
|
+
smartbox,
|
|
390
|
+
[*DEFAULT_ARGS, "device-connected-status"],
|
|
391
|
+
)
|
|
392
|
+
assert result.exit_code == 0
|
|
393
|
+
assert "connected" in result.output
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
|
-
from smartbox import ResailerNotExistError
|
|
3
|
+
from smartbox import AsyncSmartboxSession, ResailerNotExistError
|
|
4
4
|
from smartbox.resailer import AvailableResailers
|
|
5
5
|
|
|
6
6
|
|
|
@@ -44,3 +44,15 @@ def test_resailer_invalid_data():
|
|
|
44
44
|
web_url="invalid-url",
|
|
45
45
|
serial_id="invalid-serial-id",
|
|
46
46
|
).resailer
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def test_all_resailers():
|
|
51
|
+
for resailer in AvailableResailers.resailers.values():
|
|
52
|
+
_session = AsyncSmartboxSession(
|
|
53
|
+
username="", password="", api_name=resailer.api_url
|
|
54
|
+
)
|
|
55
|
+
check = await _session.health_check()
|
|
56
|
+
assert check is not None
|
|
57
|
+
version = await _session.api_version()
|
|
58
|
+
assert "major" in version
|
|
@@ -9,7 +9,7 @@ from pydantic import ValidationError
|
|
|
9
9
|
import pytest
|
|
10
10
|
|
|
11
11
|
from smartbox import APIUnavailableError, InvalidAuthError, SmartboxError
|
|
12
|
-
from smartbox.models import NodeSetup
|
|
12
|
+
from smartbox.models import DefaultNodeSetup, NodeSetup
|
|
13
13
|
from smartbox.session import (
|
|
14
14
|
_DEFAULT_BACKOFF_FACTOR,
|
|
15
15
|
_DEFAULT_RETRY_ATTEMPTS,
|
|
@@ -90,11 +90,12 @@ async def test_get_node_status(async_smartbox_session, caplog):
|
|
|
90
90
|
mock_api_request.assert_called_with(url)
|
|
91
91
|
|
|
92
92
|
async_smartbox_session.raw_response = False
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
if mock_node["type"] != "pmo":
|
|
94
|
+
status_model = await async_smartbox_session.get_node_status(
|
|
95
|
+
mock_device["dev_id"],
|
|
96
|
+
mock_node,
|
|
97
|
+
)
|
|
98
|
+
assert status_model.act_duty == status["act_duty"]
|
|
98
99
|
with pytest.raises(ValidationError):
|
|
99
100
|
mock_api_request.return_value = {
|
|
100
101
|
"sync_status": "synced",
|
|
@@ -145,9 +146,8 @@ async def test_get_node_samples(async_smartbox_session):
|
|
|
145
146
|
start_time=start_time,
|
|
146
147
|
end_time=end_time,
|
|
147
148
|
)
|
|
148
|
-
assert (
|
|
149
|
-
|
|
150
|
-
== samples["samples"][0]["counter"]
|
|
149
|
+
assert samples_model.samples[0].counter == float(
|
|
150
|
+
samples["samples"][0]["counter"]
|
|
151
151
|
)
|
|
152
152
|
async_smartbox_session.raw_response = True
|
|
153
153
|
|
|
@@ -223,6 +223,7 @@ async def test_get_device_power_limit(async_smartbox_session):
|
|
|
223
223
|
"devs/test_device/htr_system/power_limit",
|
|
224
224
|
)
|
|
225
225
|
|
|
226
|
+
mock_api_request.return_value = {"power": "100"}
|
|
226
227
|
power_limit = await async_smartbox_session.get_device_power_limit(
|
|
227
228
|
device_id="test_device", node=mock_node
|
|
228
229
|
)
|
|
@@ -921,6 +922,54 @@ async def test_health_check_api_unavailable(async_session):
|
|
|
921
922
|
)
|
|
922
923
|
|
|
923
924
|
|
|
925
|
+
@pytest.mark.asyncio
|
|
926
|
+
async def test_api_version_success(async_session):
|
|
927
|
+
with patch.object(
|
|
928
|
+
async_session.client,
|
|
929
|
+
"get",
|
|
930
|
+
new_callable=AsyncMock,
|
|
931
|
+
) as mock_get:
|
|
932
|
+
mock_response = AsyncMock()
|
|
933
|
+
mock_response.json = AsyncMock(
|
|
934
|
+
return_value={
|
|
935
|
+
"major": "1",
|
|
936
|
+
"minor": "53",
|
|
937
|
+
"subminor": "2",
|
|
938
|
+
"commit": "NULL",
|
|
939
|
+
}
|
|
940
|
+
)
|
|
941
|
+
mock_response.raise_for_status = AsyncMock()
|
|
942
|
+
mock_get.return_value = mock_response
|
|
943
|
+
|
|
944
|
+
result = await async_session.api_version()
|
|
945
|
+
assert result == {
|
|
946
|
+
"major": "1",
|
|
947
|
+
"minor": "53",
|
|
948
|
+
"subminor": "2",
|
|
949
|
+
"commit": "NULL",
|
|
950
|
+
}
|
|
951
|
+
mock_get.assert_called_once_with(
|
|
952
|
+
f"{async_session._api_host}/version",
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
@pytest.mark.asyncio
|
|
957
|
+
async def test_api_version_unavailable(async_session):
|
|
958
|
+
with patch.object(
|
|
959
|
+
async_session.client,
|
|
960
|
+
"get",
|
|
961
|
+
new_callable=AsyncMock,
|
|
962
|
+
) as mock_get:
|
|
963
|
+
mock_get.side_effect = aiohttp.ClientConnectionError()
|
|
964
|
+
|
|
965
|
+
with pytest.raises(APIUnavailableError):
|
|
966
|
+
await async_session.api_version()
|
|
967
|
+
|
|
968
|
+
mock_get.assert_called_once_with(
|
|
969
|
+
f"{async_session._api_host}/version",
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
|
|
924
973
|
@pytest.mark.asyncio
|
|
925
974
|
async def test_api_request_success(async_session):
|
|
926
975
|
path = "test_path"
|
|
@@ -1256,7 +1305,8 @@ async def test_get_node_setup(async_smartbox_session, caplog):
|
|
|
1256
1305
|
device_id=mock_device_id,
|
|
1257
1306
|
node=mock_node,
|
|
1258
1307
|
)
|
|
1259
|
-
|
|
1308
|
+
if isinstance(setup_model, DefaultNodeSetup):
|
|
1309
|
+
assert setup_model.away_mode == setup["away_mode"]
|
|
1260
1310
|
with pytest.raises(ValidationError):
|
|
1261
1311
|
mock_api_request.return_value = {
|
|
1262
1312
|
"sync_status": "synced",
|
|
@@ -1396,3 +1446,30 @@ async def test_get_home_guests(async_smartbox_session):
|
|
|
1396
1446
|
)
|
|
1397
1447
|
assert guests_model.guest_users[0].email == guests[0]["email"]
|
|
1398
1448
|
async_smartbox_session.raw_response = True
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
@pytest.mark.asyncio
|
|
1452
|
+
async def test_get_deviceconnected_status(async_smartbox_session):
|
|
1453
|
+
for mock_device in await async_smartbox_session.get_devices():
|
|
1454
|
+
with patch.object(
|
|
1455
|
+
async_smartbox_session,
|
|
1456
|
+
"_api_request",
|
|
1457
|
+
new_callable=AsyncMock,
|
|
1458
|
+
) as mock_api_request:
|
|
1459
|
+
url = f"devs/{mock_device['dev_id']}/connected"
|
|
1460
|
+
mock_api_request.return_value = await fake_get_request(
|
|
1461
|
+
mock_api_request,
|
|
1462
|
+
url,
|
|
1463
|
+
)
|
|
1464
|
+
nodes = await async_smartbox_session.get_device_connected(
|
|
1465
|
+
device_id=mock_device["dev_id"],
|
|
1466
|
+
)
|
|
1467
|
+
assert nodes == mock_api_request.return_value
|
|
1468
|
+
mock_api_request.assert_called_with(url)
|
|
1469
|
+
|
|
1470
|
+
async_smartbox_session.raw_response = False
|
|
1471
|
+
nodes_model = await async_smartbox_session.get_device_connected(
|
|
1472
|
+
device_id=mock_device["dev_id"],
|
|
1473
|
+
)
|
|
1474
|
+
assert nodes_model.connected == nodes["connected"]
|
|
1475
|
+
async_smartbox_session.raw_response = True
|
|
@@ -206,3 +206,21 @@ async def test_update_manager_run(update_manager):
|
|
|
206
206
|
) as mock_run:
|
|
207
207
|
await update_manager.run()
|
|
208
208
|
mock_run.assert_awaited_once()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_update_manager_subscribe_to_device_connected(update_manager):
|
|
212
|
+
callback = MagicMock()
|
|
213
|
+
update_manager.subscribe_to_device_connected(callback)
|
|
214
|
+
assert len(update_manager._dev_data_subscriptions) == 1
|
|
215
|
+
assert len(update_manager._update_subscriptions) == 1
|
|
216
|
+
|
|
217
|
+
# Test dev data callback
|
|
218
|
+
dev_data = {"connected": True}
|
|
219
|
+
update_manager._dev_data_cb(dev_data)
|
|
220
|
+
callback.assert_called_once_with(dev_data["connected"])
|
|
221
|
+
|
|
222
|
+
# Test update callback
|
|
223
|
+
callback.reset_mock()
|
|
224
|
+
update_data = {"path": "/connected", "body": {"connected": True}}
|
|
225
|
+
update_manager._update_cb(update_data)
|
|
226
|
+
callback.assert_called_once_with(update_data["body"]["connected"])
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
+
revision = 1
|
|
2
3
|
requires-python = ">=3.13"
|
|
3
4
|
|
|
4
5
|
[[package]]
|
|
@@ -684,7 +685,7 @@ wheels = [
|
|
|
684
685
|
|
|
685
686
|
[[package]]
|
|
686
687
|
name = "smartbox"
|
|
687
|
-
version = "2.1.
|
|
688
|
+
version = "2.1.3"
|
|
688
689
|
source = { editable = "." }
|
|
689
690
|
dependencies = [
|
|
690
691
|
{ name = "aiohttp" },
|
smartbox-2.1.2/requirements.txt
DELETED
|
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
|
|
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
|