pyimouapi 1.3.1__tar.gz → 1.3.2__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.
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/PKG-INFO +1 -1
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/__init__.py +1 -1
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/device.py +17 -8
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/ha_device.py +14 -4
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi.egg-info/PKG-INFO +1 -1
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi.egg-info/SOURCES.txt +1 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyproject.toml +1 -1
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/setup.py +1 -1
- pyimouapi-1.3.2/tests/test_channel_id_and_api_guards.py +97 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/LICENSE +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/README.md +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/const.py +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/exceptions.py +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/openapi.py +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/sensor.py +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi.egg-info/dependency_links.txt +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi.egg-info/requires.txt +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi.egg-info/top_level.txt +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/setup.cfg +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_collect_property_entities.py +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_device_summaries.py +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_event_identifier_map.py +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_lookup_property.py +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_message_callback.py +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_sensor_normalize.py +0 -0
- {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_update_from_detail.py +0 -0
|
@@ -258,6 +258,10 @@ class ImouDeviceManager:
|
|
|
258
258
|
self._event_map_locks: dict[str, asyncio.Lock] = {}
|
|
259
259
|
self._event_map_locks_guard = asyncio.Lock()
|
|
260
260
|
|
|
261
|
+
async def async_close(self) -> None:
|
|
262
|
+
"""Close the underlying Open API HTTP session."""
|
|
263
|
+
await self._imou_api_client.async_close()
|
|
264
|
+
|
|
261
265
|
async def _async_lock_for_product(self, product_id: str) -> asyncio.Lock:
|
|
262
266
|
async with self._event_map_locks_guard:
|
|
263
267
|
lock = self._event_map_locks.get(product_id)
|
|
@@ -555,11 +559,13 @@ class ImouDeviceManager:
|
|
|
555
559
|
}
|
|
556
560
|
]
|
|
557
561
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
562
|
+
data = await self._imou_api_client.async_request_api(
|
|
563
|
+
API_ENDPOINT_GET_IOT_DEVICE_PROPERTIES, params
|
|
564
|
+
)
|
|
565
|
+
device_list = data.get(PARAM_DEVICE_LIST) if isinstance(data, dict) else None
|
|
566
|
+
if not device_list:
|
|
567
|
+
return {}
|
|
568
|
+
return device_list[0]
|
|
563
569
|
|
|
564
570
|
async def async_set_iot_device_properties(
|
|
565
571
|
self, device_id: str, channel_id: str | None, product_id: str, properties: dict
|
|
@@ -655,12 +661,15 @@ class ImouDeviceManager:
|
|
|
655
661
|
if device_detail.get(PARAM_CHANNELS) and imou_device.channels:
|
|
656
662
|
channels_detail = device_detail.get(PARAM_CHANNELS)
|
|
657
663
|
channel_detail_map = {
|
|
658
|
-
channel_detail.get(PARAM_CHANNEL_ID): channel_detail.get(
|
|
664
|
+
str(channel_detail.get(PARAM_CHANNEL_ID)): channel_detail.get(
|
|
659
665
|
PARAM_ABILITY_REFS, "unknown"
|
|
660
666
|
)
|
|
661
667
|
for channel_detail in channels_detail
|
|
668
|
+
if channel_detail.get(PARAM_CHANNEL_ID) is not None
|
|
662
669
|
}
|
|
663
670
|
for channel in imou_device.channels:
|
|
664
|
-
channel_id =
|
|
665
|
-
|
|
671
|
+
channel_id = (
|
|
672
|
+
str(channel.channel_id) if channel.channel_id is not None else None
|
|
673
|
+
)
|
|
674
|
+
if channel_id is not None and channel_id in channel_detail_map:
|
|
666
675
|
channel.set_channel_ability_refs(channel_detail_map[channel_id])
|
|
@@ -254,6 +254,10 @@ class ImouHaDeviceManager:
|
|
|
254
254
|
def delegate(self):
|
|
255
255
|
return self._delegate
|
|
256
256
|
|
|
257
|
+
async def async_close(self) -> None:
|
|
258
|
+
"""Close the underlying Open API HTTP session."""
|
|
259
|
+
await self._delegate.async_close()
|
|
260
|
+
|
|
257
261
|
@staticmethod
|
|
258
262
|
def _resolve_device_id(device: ImouHaDevice) -> str:
|
|
259
263
|
device_id = device.device_id
|
|
@@ -482,8 +486,11 @@ class ImouHaDeviceManager:
|
|
|
482
486
|
self.get_device_status(data[PARAM_ONLINE]),
|
|
483
487
|
)
|
|
484
488
|
else:
|
|
489
|
+
device_channel_id = (
|
|
490
|
+
str(device.channel_id) if device.channel_id is not None else None
|
|
491
|
+
)
|
|
485
492
|
for channel in data[PARAM_CHANNELS]:
|
|
486
|
-
if channel[PARAM_CHANNEL_ID] ==
|
|
493
|
+
if str(channel[PARAM_CHANNEL_ID]) == device_channel_id:
|
|
487
494
|
apply_sensor_state(
|
|
488
495
|
device.sensors,
|
|
489
496
|
PARAM_STATUS,
|
|
@@ -681,9 +688,12 @@ class ImouHaDeviceManager:
|
|
|
681
688
|
+ "_"
|
|
682
689
|
+ device.parent_product_id
|
|
683
690
|
)
|
|
684
|
-
if value_type == "int"
|
|
685
|
-
|
|
686
|
-
|
|
691
|
+
if value_type == "int":
|
|
692
|
+
try:
|
|
693
|
+
value = int(text_value)
|
|
694
|
+
except (TypeError, ValueError):
|
|
695
|
+
value = text_value
|
|
696
|
+
elif value_type == "str":
|
|
687
697
|
value = str(text_value)
|
|
688
698
|
else:
|
|
689
699
|
value = text_value
|
|
@@ -14,6 +14,7 @@ pyimouapi.egg-info/SOURCES.txt
|
|
|
14
14
|
pyimouapi.egg-info/dependency_links.txt
|
|
15
15
|
pyimouapi.egg-info/requires.txt
|
|
16
16
|
pyimouapi.egg-info/top_level.txt
|
|
17
|
+
tests/test_channel_id_and_api_guards.py
|
|
17
18
|
tests/test_collect_property_entities.py
|
|
18
19
|
tests/test_device_summaries.py
|
|
19
20
|
tests/test_event_identifier_map.py
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Guards for channelId type mismatch and empty API payloads."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from pyimouapi.const import (
|
|
7
|
+
PARAM_ABILITY_REFS,
|
|
8
|
+
PARAM_CHANNEL_ID,
|
|
9
|
+
PARAM_CHANNELS,
|
|
10
|
+
PARAM_DEVICE_LIST,
|
|
11
|
+
PARAM_ONLINE,
|
|
12
|
+
PARAM_REF,
|
|
13
|
+
PARAM_STATE,
|
|
14
|
+
PARAM_STATUS,
|
|
15
|
+
PARAM_VALUE_TYPE,
|
|
16
|
+
)
|
|
17
|
+
from pyimouapi.device import ImouChannel, ImouDevice, ImouDeviceManager
|
|
18
|
+
from pyimouapi.ha_device import DeviceStatus, ImouHaDevice, ImouHaDeviceManager
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.asyncio
|
|
22
|
+
async def test_online_status_matches_int_channel_id():
|
|
23
|
+
"""API may return channelId as int while HA device stores str."""
|
|
24
|
+
device = ImouHaDevice("dev1", "Cam", "Imou", "IPC", "1.0")
|
|
25
|
+
device.set_channel_id("0")
|
|
26
|
+
device.sensors[PARAM_STATUS][PARAM_STATE] = DeviceStatus.OFFLINE.value
|
|
27
|
+
|
|
28
|
+
delegate = MagicMock()
|
|
29
|
+
delegate.async_get_device_online_status = AsyncMock(
|
|
30
|
+
return_value={
|
|
31
|
+
PARAM_ONLINE: "1",
|
|
32
|
+
PARAM_CHANNELS: [
|
|
33
|
+
{PARAM_CHANNEL_ID: 0, PARAM_ONLINE: "1"},
|
|
34
|
+
],
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
manager = ImouHaDeviceManager(delegate)
|
|
38
|
+
await manager._async_update_status(device)
|
|
39
|
+
|
|
40
|
+
assert device.sensors[PARAM_STATUS][PARAM_STATE] == DeviceStatus.ONLINE.value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_ability_refs_match_int_channel_id():
|
|
45
|
+
"""Channel abilityRefs lookup must tolerate int/str channelId."""
|
|
46
|
+
channel = ImouChannel("0", "ch0", "1", "ability")
|
|
47
|
+
device = ImouDevice("dev1", "Cam", "1", "Imou", "IPC")
|
|
48
|
+
device.set_channels([channel])
|
|
49
|
+
device.set_product_id("pid1")
|
|
50
|
+
|
|
51
|
+
manager = ImouDeviceManager(MagicMock())
|
|
52
|
+
manager.async_get_iot_device_detail_info = AsyncMock(
|
|
53
|
+
return_value={
|
|
54
|
+
PARAM_ABILITY_REFS: "devAbility",
|
|
55
|
+
PARAM_CHANNELS: [
|
|
56
|
+
{PARAM_CHANNEL_ID: 0, PARAM_ABILITY_REFS: "chAbility"},
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
await manager._async_update_device_ability_refs(device)
|
|
61
|
+
|
|
62
|
+
assert channel.channel_ability_refs == "chAbility"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.asyncio
|
|
66
|
+
async def test_get_iot_device_properties_empty_list():
|
|
67
|
+
"""Empty deviceList must not raise IndexError."""
|
|
68
|
+
api = MagicMock()
|
|
69
|
+
api.async_request_api = AsyncMock(return_value={PARAM_DEVICE_LIST: []})
|
|
70
|
+
manager = ImouDeviceManager(api)
|
|
71
|
+
|
|
72
|
+
result = await manager.async_get_iot_device_properties(
|
|
73
|
+
"dev1", "0", "pid", ["10001"]
|
|
74
|
+
)
|
|
75
|
+
assert result == {}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio
|
|
79
|
+
async def test_set_text_value_coerces_str_type():
|
|
80
|
+
"""value_type=str must stringify the payload."""
|
|
81
|
+
device = ImouHaDevice("dev1", "Plug", "Imou", "Plug", "1.0")
|
|
82
|
+
device.set_product_id("pid1")
|
|
83
|
+
device.texts["name"] = {
|
|
84
|
+
PARAM_REF: "20001",
|
|
85
|
+
PARAM_VALUE_TYPE: "str",
|
|
86
|
+
PARAM_STATE: "",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
delegate = MagicMock()
|
|
90
|
+
delegate.async_set_iot_device_properties = AsyncMock()
|
|
91
|
+
manager = ImouHaDeviceManager(delegate)
|
|
92
|
+
|
|
93
|
+
await manager.async_set_text_value(device, "name", "hello")
|
|
94
|
+
|
|
95
|
+
delegate.async_set_iot_device_properties.assert_awaited_with(
|
|
96
|
+
"dev1", None, "pid1", {"20001": "hello"}
|
|
97
|
+
)
|
|
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
|