pyimouapi 1.3.1__tar.gz → 1.3.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.
Files changed (31) hide show
  1. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/PKG-INFO +4 -3
  2. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/README.md +3 -2
  3. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyimouapi/__init__.py +1 -1
  4. pyimouapi-1.3.3/pyimouapi/collection_point.py +89 -0
  5. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyimouapi/const.py +42 -0
  6. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyimouapi/device.py +65 -8
  7. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyimouapi/ha_device.py +175 -28
  8. pyimouapi-1.3.3/pyimouapi/siren.py +15 -0
  9. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyimouapi.egg-info/PKG-INFO +4 -3
  10. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyimouapi.egg-info/SOURCES.txt +7 -1
  11. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyproject.toml +1 -1
  12. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/setup.py +1 -1
  13. pyimouapi-1.3.3/tests/test_channel_id_and_api_guards.py +97 -0
  14. pyimouapi-1.3.3/tests/test_collection_point.py +205 -0
  15. pyimouapi-1.3.3/tests/test_siren.py +272 -0
  16. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/tests/test_update_from_detail.py +3 -5
  17. pyimouapi-1.3.3/tests/test_write_optimistic_state.py +60 -0
  18. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/LICENSE +0 -0
  19. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyimouapi/exceptions.py +0 -0
  20. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyimouapi/openapi.py +0 -0
  21. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyimouapi/sensor.py +0 -0
  22. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyimouapi.egg-info/dependency_links.txt +0 -0
  23. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyimouapi.egg-info/requires.txt +0 -0
  24. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/pyimouapi.egg-info/top_level.txt +0 -0
  25. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/setup.cfg +0 -0
  26. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/tests/test_collect_property_entities.py +0 -0
  27. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/tests/test_device_summaries.py +0 -0
  28. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/tests/test_event_identifier_map.py +0 -0
  29. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/tests/test_lookup_property.py +0 -0
  30. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/tests/test_message_callback.py +0 -0
  31. {pyimouapi-1.3.1 → pyimouapi-1.3.3}/tests/test_sensor_normalize.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.3
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
@@ -30,8 +30,9 @@ Async Python client for the **Imou Open Platform** cloud APIs. Built on **aiohtt
30
30
  | Module | Role |
31
31
  |--------|------|
32
32
  | `pyimouapi.openapi` | `ImouOpenApiClient` — auth, signing, token lifecycle, HTTP calls |
33
- | `pyimouapi.device` | `ImouDeviceManager`, `ImouDevice`, `ImouChannel` — listing, PTZ, alarms, storage, and related endpoints |
34
- | `pyimouapi.ha_device` | `ImouHaDeviceManager`, `ImouHaDevice`, … — aggregated “device model” helpers for automation stacks |
33
+ | `pyimouapi.device` | `ImouDeviceManager`, `ImouDevice`, `ImouChannel` — listing, PTZ, alarms, storage, collection points (`getCollection` / `turnCollection`), and related endpoints |
34
+ | `pyimouapi.collection_point` | Parsing helpers for PaaS / IoT collection point (preset) name lists |
35
+ | `pyimouapi.ha_device` | `ImouHaDeviceManager`, `ImouHaDevice`, … — aggregated “device model” helpers for automation stacks (including `select.collection_point` when the device exposes `CollectionPoint` or IoT refs `21500`/`22000`) |
35
36
  | `pyimouapi.exceptions` | `ImouException` and typed errors (connect, request, invalid credentials, …) |
36
37
 
37
38
  The top-level `pyimouapi` package re-exports common symbols. Import submodules directly when needed, for example `from pyimouapi.ha_device import ImouHaDeviceManager`.
@@ -11,8 +11,9 @@ Async Python client for the **Imou Open Platform** cloud APIs. Built on **aiohtt
11
11
  | Module | Role |
12
12
  |--------|------|
13
13
  | `pyimouapi.openapi` | `ImouOpenApiClient` — auth, signing, token lifecycle, HTTP calls |
14
- | `pyimouapi.device` | `ImouDeviceManager`, `ImouDevice`, `ImouChannel` — listing, PTZ, alarms, storage, and related endpoints |
15
- | `pyimouapi.ha_device` | `ImouHaDeviceManager`, `ImouHaDevice`, … — aggregated “device model” helpers for automation stacks |
14
+ | `pyimouapi.device` | `ImouDeviceManager`, `ImouDevice`, `ImouChannel` — listing, PTZ, alarms, storage, collection points (`getCollection` / `turnCollection`), and related endpoints |
15
+ | `pyimouapi.collection_point` | Parsing helpers for PaaS / IoT collection point (preset) name lists |
16
+ | `pyimouapi.ha_device` | `ImouHaDeviceManager`, `ImouHaDevice`, … — aggregated “device model” helpers for automation stacks (including `select.collection_point` when the device exposes `CollectionPoint` or IoT refs `21500`/`22000`) |
16
17
  | `pyimouapi.exceptions` | `ImouException` and typed errors (connect, request, invalid credentials, …) |
17
18
 
18
19
  The top-level `pyimouapi` package re-exports common symbols. Import submodules directly when needed, for example `from pyimouapi.ha_device import ImouHaDeviceManager`.
@@ -1,4 +1,4 @@
1
- __version__ = "1.3.1"
1
+ __version__ = "1.3.3"
2
2
 
3
3
  from .device import ImouChannel, ImouDevice, ImouDeviceManager
4
4
  from .exceptions import (
@@ -0,0 +1,89 @@
1
+ """Collection point (PTZ preset) parsing helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .const import IOT_COLLECTION_NAME_REF, PARAM_COLLECTION_POINT_PROMPT
8
+
9
+
10
+ def parse_paas_collection_names(data: dict[str, Any]) -> list[str]:
11
+ """Extract preset names from getCollection PaaS response data."""
12
+ names: list[str] = []
13
+ for item in data.get("collections") or []:
14
+ if not isinstance(item, dict):
15
+ continue
16
+ name = item.get("name")
17
+ if name and str(name) != PARAM_COLLECTION_POINT_PROMPT:
18
+ names.append(str(name))
19
+ return unique_preserve_order(names)
20
+
21
+
22
+ def _name_from_collection_item(item: dict[str, Any]) -> str | None:
23
+ for key in ("Name", "name", IOT_COLLECTION_NAME_REF):
24
+ value = item.get(key)
25
+ if value:
26
+ return str(value)
27
+ return None
28
+
29
+
30
+ def parse_iot_collection_names(output_data: Any) -> list[str]:
31
+ """Extract preset names from GetCollection IoT outputData."""
32
+ names: list[str] = []
33
+ if output_data is None:
34
+ return names
35
+
36
+ if isinstance(output_data, dict):
37
+ for value in output_data.values():
38
+ if isinstance(value, list):
39
+ names.extend(_names_from_collection_items(value))
40
+ elif isinstance(value, str) and value != PARAM_COLLECTION_POINT_PROMPT:
41
+ names.append(value)
42
+ return unique_preserve_order(names)
43
+
44
+ if isinstance(output_data, list):
45
+ if output_data and all(isinstance(item, dict) for item in output_data):
46
+ if any(
47
+ item.get("identifier") == "collection"
48
+ or IOT_COLLECTION_NAME_REF in item
49
+ or "Name" in item
50
+ for item in output_data
51
+ ):
52
+ names.extend(_names_from_collection_items(output_data))
53
+ else:
54
+ for item in output_data:
55
+ if not isinstance(item, dict):
56
+ continue
57
+ nested = item.get("value")
58
+ if isinstance(nested, list):
59
+ names.extend(_names_from_collection_items(nested))
60
+ return unique_preserve_order(names)
61
+
62
+ return names
63
+
64
+
65
+ def _names_from_collection_items(items: list[Any]) -> list[str]:
66
+ names: list[str] = []
67
+ for item in items:
68
+ if not isinstance(item, dict):
69
+ continue
70
+ name = _name_from_collection_item(item)
71
+ if name and name != PARAM_COLLECTION_POINT_PROMPT:
72
+ names.append(name)
73
+ return names
74
+
75
+
76
+ def unique_preserve_order(names: list[str]) -> list[str]:
77
+ seen: set[str] = set()
78
+ ordered: list[str] = []
79
+ for name in names:
80
+ if name in seen:
81
+ continue
82
+ seen.add(name)
83
+ ordered.append(name)
84
+ return ordered
85
+
86
+
87
+ def build_collection_point_options(names: list[str]) -> list[str]:
88
+ """Build select options with placeholder first, preserving API order."""
89
+ return [PARAM_COLLECTION_POINT_PROMPT, *unique_preserve_order(names)]
@@ -23,6 +23,10 @@ API_ENDPOINT_GET_PRODUCT_MODEL = "/openapi/getProductModel"
23
23
  API_ENDPOINT_GET_IOT_DEVICE_DETAIL_INFO = "/openapi/getIotDeviceDetailInfo"
24
24
  API_ENDPOINT_WAKE_UP_DEVICE = "/openapi/wakeUpDevice"
25
25
  API_ENDPOINT_SET_MESSAGE_CALLBACK = "/openapi/setMessageCallback"
26
+ API_ENDPOINT_GET_COLLECTION = "/openapi/getCollection"
27
+ API_ENDPOINT_TURN_COLLECTION = "/openapi/turnCollection"
28
+ API_ENDPOINT_SIREN_START = "/openapi/sirenStart"
29
+ API_ENDPOINT_SIREN_STOP = "/openapi/sirenStop"
26
30
 
27
31
  # error_codes
28
32
  ERROR_CODE_SUCCESS = "0"
@@ -109,6 +113,10 @@ PARAM_MOTION_DETECT = "motion_detect"
109
113
  PARAM_STORAGE_USED = "storage_used"
110
114
  PARAM_RESTART_DEVICE = "restart_device"
111
115
  PARAM_NIGHT_VISION_MODE = "night_vision_mode"
116
+ PARAM_COLLECTION_POINT = "collection_point"
117
+ PARAM_COLLECTION_POINT_PROMPT = "select_collection_point"
118
+ PARAM_SIREN_START = "siren_start"
119
+ PARAM_SIREN_STOP = "siren_stop"
112
120
  PARAM_PTZ = "ptz"
113
121
  PARAM_TEMPERATURE_CURRENT = "temperature_current"
114
122
  PARAM_HUMIDITY_CURRENT = "humidity_current"
@@ -128,6 +136,18 @@ PARAM_ABILITY_REFS = "abilityRefs"
128
136
  PARAM_REF_TYPE = "ref_type"
129
137
  PARAM_EXPRESSION = "expression"
130
138
  PARAM_OUTPUT_DATA = "outputData"
139
+ PARAM_TURN_REF = "turn_ref"
140
+ PARAM_TURN_INPUT_REF = "turn_input_ref"
141
+ PARAM_INPUT_REF = "input_ref"
142
+ PARAM_COLLECTION_NAME = "name"
143
+
144
+ IOT_GET_COLLECTION_REF = "21500"
145
+ IOT_TURN_COLLECTION_REF = "22000"
146
+ IOT_TURN_COLLECTION_NAME_REF = "22001"
147
+ IOT_COLLECTION_NAME_REF = "21551"
148
+ IOT_SIREN_START_REF = "25500"
149
+ IOT_SIREN_START_INPUT_REF = "25501"
150
+ IOT_SIREN_STOP_REF = "22200"
131
151
  PARAM_VALUE_TYPE = "value_type"
132
152
  PARAM_ACCESS_TYPE = "accessType"
133
153
  PARAM_ABILITY = "ability"
@@ -294,6 +314,8 @@ SWITCH_TYPE_REF = {
294
314
  # Required capacity for various button types
295
315
  BUTTON_TYPE_ABILITY = {
296
316
  "restart_device": ["Reboot"],
317
+ "siren_start": ["Siren"],
318
+ "siren_stop": ["Siren"],
297
319
  "ptz_up": ["PT", "PTZ"],
298
320
  "ptz_down": ["PT", "PTZ"],
299
321
  "ptz_left": ["PT", "PTZ"],
@@ -331,6 +353,15 @@ BUTTON_TYPE_REF = {
331
353
  ],
332
354
  },
333
355
  ],
356
+ "siren_start": [
357
+ {
358
+ "ref": IOT_SIREN_START_REF,
359
+ "input_ref": IOT_SIREN_START_INPUT_REF,
360
+ },
361
+ ],
362
+ "siren_stop": [
363
+ {"ref": IOT_SIREN_STOP_REF},
364
+ ],
334
365
  "ptz_up": [
335
366
  {"ref": "22100"},
336
367
  {"ref": "88700"},
@@ -371,6 +402,7 @@ BUTTON_TYPE_REF = {
371
402
  # Required capacity for various select types
372
403
  SELECT_TYPE_ABILITY = {
373
404
  "night_vision_mode": ["NVM"],
405
+ "collection_point": ["CollectionPoint"],
374
406
  }
375
407
  SELECT_TYPE_REF = {
376
408
  "night_vision_mode": [
@@ -404,6 +436,16 @@ SELECT_TYPE_REF = {
404
436
  "value_type": "int",
405
437
  }
406
438
  ],
439
+ "collection_point": [
440
+ {
441
+ "ref": IOT_GET_COLLECTION_REF,
442
+ "turn_ref": IOT_TURN_COLLECTION_REF,
443
+ "turn_input_ref": IOT_TURN_COLLECTION_NAME_REF,
444
+ "ref_type": "services",
445
+ "default": PARAM_COLLECTION_POINT_PROMPT,
446
+ "options": [PARAM_COLLECTION_POINT_PROMPT],
447
+ }
448
+ ],
407
449
  }
408
450
  # Required capacity for various sensor types
409
451
  SENSOR_TYPE_ABILITY = {
@@ -10,6 +10,7 @@ from .const import (
10
10
  API_ENDPOINT_CONTROL_DEVICE_PTZ,
11
11
  API_ENDPOINT_DEVICE_SD_CARD_STATUS,
12
12
  API_ENDPOINT_DEVICE_STORAGE,
13
+ API_ENDPOINT_GET_COLLECTION,
13
14
  API_ENDPOINT_GET_DEVICE_ALARM_PARAM,
14
15
  API_ENDPOINT_GET_DEVICE_LIVE_INFO,
15
16
  API_ENDPOINT_GET_DEVICE_NIGHT_VISION_MODE,
@@ -27,6 +28,9 @@ from .const import (
27
28
  API_ENDPOINT_SET_DEVICE_SNAP,
28
29
  API_ENDPOINT_SET_DEVICE_STATUS,
29
30
  API_ENDPOINT_SET_IOT_DEVICE_PROPERTIES,
31
+ API_ENDPOINT_SIREN_START,
32
+ API_ENDPOINT_SIREN_STOP,
33
+ API_ENDPOINT_TURN_COLLECTION,
30
34
  API_ENDPOINT_WAKE_UP_DEVICE,
31
35
  NIGHT_VISION_MODE_MAP,
32
36
  PARAM_ABILITY_REFS,
@@ -39,6 +43,7 @@ from .const import (
39
43
  PARAM_CHANNEL_NUM,
40
44
  PARAM_CHANNEL_STATUS,
41
45
  PARAM_CHANNELS,
46
+ PARAM_COLLECTION_NAME,
42
47
  PARAM_CONTENT,
43
48
  PARAM_COUNT,
44
49
  PARAM_DEVICE_ABILITY,
@@ -206,6 +211,10 @@ class ImouDevice:
206
211
  def parent_device_id(self) -> str | None:
207
212
  return self._parent_device_id
208
213
 
214
+ @property
215
+ def channel_number(self) -> int:
216
+ return self._channel_number
217
+
209
218
  @property
210
219
  def is_ipc(self) -> bool:
211
220
  return (
@@ -258,6 +267,10 @@ class ImouDeviceManager:
258
267
  self._event_map_locks: dict[str, asyncio.Lock] = {}
259
268
  self._event_map_locks_guard = asyncio.Lock()
260
269
 
270
+ async def async_close(self) -> None:
271
+ """Close the underlying Open API HTTP session."""
272
+ await self._imou_api_client.async_close()
273
+
261
274
  async def _async_lock_for_product(self, product_id: str) -> asyncio.Lock:
262
275
  async with self._event_map_locks_guard:
263
276
  lock = self._event_map_locks.get(product_id)
@@ -555,11 +568,13 @@ class ImouDeviceManager:
555
568
  }
556
569
  ]
557
570
  }
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]
571
+ data = await self._imou_api_client.async_request_api(
572
+ API_ENDPOINT_GET_IOT_DEVICE_PROPERTIES, params
573
+ )
574
+ device_list = data.get(PARAM_DEVICE_LIST) if isinstance(data, dict) else None
575
+ if not device_list:
576
+ return {}
577
+ return device_list[0]
563
578
 
564
579
  async def async_set_iot_device_properties(
565
580
  self, device_id: str, channel_id: str | None, product_id: str, properties: dict
@@ -635,6 +650,45 @@ class ImouDeviceManager:
635
650
  API_ENDPOINT_GET_IOT_DEVICE_DETAIL_INFO, params
636
651
  )
637
652
 
653
+ async def async_get_device_collection(
654
+ self, device_id: str, channel_id: str
655
+ ) -> dict[str, Any]:
656
+ params = {
657
+ PARAM_DEVICE_ID: device_id,
658
+ PARAM_CHANNEL_ID: channel_id,
659
+ }
660
+ return await self._imou_api_client.async_request_api(
661
+ API_ENDPOINT_GET_COLLECTION, params
662
+ )
663
+
664
+ async def async_turn_device_collection(
665
+ self, device_id: str, channel_id: str, name: str
666
+ ) -> None:
667
+ params = {
668
+ PARAM_DEVICE_ID: device_id,
669
+ PARAM_CHANNEL_ID: channel_id,
670
+ PARAM_COLLECTION_NAME: name,
671
+ }
672
+ await self._imou_api_client.async_request_api(
673
+ API_ENDPOINT_TURN_COLLECTION, params
674
+ )
675
+
676
+ async def async_siren_start(
677
+ self, device_id: str, channels: list[int] | None = None
678
+ ) -> None:
679
+ params: dict[str, Any] = {PARAM_DEVICE_ID: device_id}
680
+ if channels is not None:
681
+ params[PARAM_CHANNELS] = channels
682
+ await self._imou_api_client.async_request_api(API_ENDPOINT_SIREN_START, params)
683
+
684
+ async def async_siren_stop(
685
+ self, device_id: str, channels: list[int] | None = None
686
+ ) -> None:
687
+ params: dict[str, Any] = {PARAM_DEVICE_ID: device_id}
688
+ if channels is not None:
689
+ params[PARAM_CHANNELS] = channels
690
+ await self._imou_api_client.async_request_api(API_ENDPOINT_SIREN_STOP, params)
691
+
638
692
  async def _async_update_device_ability_refs(self, imou_device: ImouDevice) -> None:
639
693
  device_id = imou_device.device_id
640
694
  if imou_device.parent_product_id is not None:
@@ -655,12 +709,15 @@ class ImouDeviceManager:
655
709
  if device_detail.get(PARAM_CHANNELS) and imou_device.channels:
656
710
  channels_detail = device_detail.get(PARAM_CHANNELS)
657
711
  channel_detail_map = {
658
- channel_detail.get(PARAM_CHANNEL_ID): channel_detail.get(
712
+ str(channel_detail.get(PARAM_CHANNEL_ID)): channel_detail.get(
659
713
  PARAM_ABILITY_REFS, "unknown"
660
714
  )
661
715
  for channel_detail in channels_detail
716
+ if channel_detail.get(PARAM_CHANNEL_ID) is not None
662
717
  }
663
718
  for channel in imou_device.channels:
664
- channel_id = channel.channel_id
665
- if channel_id in channel_detail_map:
719
+ channel_id = (
720
+ str(channel.channel_id) if channel.channel_id is not None else None
721
+ )
722
+ if channel_id is not None and channel_id in channel_detail_map:
666
723
  channel.set_channel_ability_refs(channel_detail_map[channel_id])