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.
Files changed (26) hide show
  1. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/PKG-INFO +1 -1
  2. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/__init__.py +1 -1
  3. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/device.py +17 -8
  4. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/ha_device.py +14 -4
  5. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi.egg-info/PKG-INFO +1 -1
  6. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi.egg-info/SOURCES.txt +1 -0
  7. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyproject.toml +1 -1
  8. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/setup.py +1 -1
  9. pyimouapi-1.3.2/tests/test_channel_id_and_api_guards.py +97 -0
  10. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/LICENSE +0 -0
  11. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/README.md +0 -0
  12. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/const.py +0 -0
  13. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/exceptions.py +0 -0
  14. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/openapi.py +0 -0
  15. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi/sensor.py +0 -0
  16. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi.egg-info/dependency_links.txt +0 -0
  17. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi.egg-info/requires.txt +0 -0
  18. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/pyimouapi.egg-info/top_level.txt +0 -0
  19. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/setup.cfg +0 -0
  20. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_collect_property_entities.py +0 -0
  21. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_device_summaries.py +0 -0
  22. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_event_identifier_map.py +0 -0
  23. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_lookup_property.py +0 -0
  24. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_message_callback.py +0 -0
  25. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_sensor_normalize.py +0 -0
  26. {pyimouapi-1.3.1 → pyimouapi-1.3.2}/tests/test_update_from_detail.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyimouapi
3
- Version: 1.3.1
3
+ Version: 1.3.2
4
4
  Summary: A package for imou open api
5
5
  Home-page: https://github.com/Imou-OpenPlatform/Py-Imou-Open-Api
6
6
  Author: Imou-OpenPlatform
@@ -1,4 +1,4 @@
1
- __version__ = "1.3.1"
1
+ __version__ = "1.3.2"
2
2
 
3
3
  from .device import ImouChannel, ImouDevice, ImouDeviceManager
4
4
  from .exceptions import (
@@ -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
- return (
559
- await self._imou_api_client.async_request_api(
560
- API_ENDPOINT_GET_IOT_DEVICE_PROPERTIES, params
561
- )
562
- ).get(PARAM_DEVICE_LIST, [{}])[0]
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 = channel.channel_id
665
- if channel_id in channel_detail_map:
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] == device.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" and text_value.isdigit():
685
- value = int(text_value)
686
- elif value_type == "str" and not isinstance(value_type, str):
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyimouapi
3
- Version: 1.3.1
3
+ Version: 1.3.2
4
4
  Summary: A package for imou open api
5
5
  Home-page: https://github.com/Imou-OpenPlatform/Py-Imou-Open-Api
6
6
  Author: Imou-OpenPlatform
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyimouapi"
7
- version = "1.3.1"
7
+ version = "1.3.2"
8
8
  description = "A package for imou open api"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
4
4
 
5
5
  setup(
6
6
  name="pyimouapi",
7
- version="1.3.1",
7
+ version="1.3.2",
8
8
  packages=find_packages(),
9
9
  python_requires=">=3.11",
10
10
  install_requires=[
@@ -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