pyimouapi 1.3.2__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.2 → pyimouapi-1.3.3}/PKG-INFO +4 -3
  2. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/README.md +3 -2
  3. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyimouapi/__init__.py +1 -1
  4. pyimouapi-1.3.3/pyimouapi/collection_point.py +89 -0
  5. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyimouapi/const.py +42 -0
  6. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyimouapi/device.py +48 -0
  7. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyimouapi/ha_device.py +161 -24
  8. pyimouapi-1.3.3/pyimouapi/siren.py +15 -0
  9. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyimouapi.egg-info/PKG-INFO +4 -3
  10. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyimouapi.egg-info/SOURCES.txt +6 -1
  11. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyproject.toml +1 -1
  12. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/setup.py +1 -1
  13. pyimouapi-1.3.3/tests/test_collection_point.py +205 -0
  14. pyimouapi-1.3.3/tests/test_siren.py +272 -0
  15. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/tests/test_update_from_detail.py +3 -5
  16. pyimouapi-1.3.3/tests/test_write_optimistic_state.py +60 -0
  17. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/LICENSE +0 -0
  18. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyimouapi/exceptions.py +0 -0
  19. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyimouapi/openapi.py +0 -0
  20. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyimouapi/sensor.py +0 -0
  21. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyimouapi.egg-info/dependency_links.txt +0 -0
  22. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyimouapi.egg-info/requires.txt +0 -0
  23. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/pyimouapi.egg-info/top_level.txt +0 -0
  24. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/setup.cfg +0 -0
  25. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/tests/test_channel_id_and_api_guards.py +0 -0
  26. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/tests/test_collect_property_entities.py +0 -0
  27. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/tests/test_device_summaries.py +0 -0
  28. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/tests/test_event_identifier_map.py +0 -0
  29. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/tests/test_lookup_property.py +0 -0
  30. {pyimouapi-1.3.2 → pyimouapi-1.3.3}/tests/test_message_callback.py +0 -0
  31. {pyimouapi-1.3.2 → 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.2
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.2"
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 (
@@ -641,6 +650,45 @@ class ImouDeviceManager:
641
650
  API_ENDPOINT_GET_IOT_DEVICE_DETAIL_INFO, params
642
651
  )
643
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
+
644
692
  async def _async_update_device_ability_refs(self, imou_device: ImouDevice) -> None:
645
693
  device_id = imou_device.device_id
646
694
  if imou_device.parent_product_id is not None:
@@ -6,6 +6,11 @@ from typing import Any
6
6
  import aiohttp
7
7
  from simpleeval import SimpleEval
8
8
 
9
+ from .collection_point import (
10
+ build_collection_point_options,
11
+ parse_iot_collection_names,
12
+ parse_paas_collection_names,
13
+ )
9
14
  from .const import (
10
15
  BINARY_SENSOR_TYPE_ABILITY,
11
16
  BINARY_SENSOR_TYPE_REF,
@@ -21,6 +26,8 @@ from .const import (
21
26
  PARAM_BATTERY,
22
27
  PARAM_CHANNEL_ID,
23
28
  PARAM_CHANNELS,
29
+ PARAM_COLLECTION_POINT,
30
+ PARAM_COLLECTION_POINT_PROMPT,
24
31
  PARAM_CONTENT,
25
32
  PARAM_CURRENT_OPTION,
26
33
  PARAM_DEFAULT,
@@ -32,6 +39,7 @@ from .const import (
32
39
  PARAM_HD,
33
40
  PARAM_HLS,
34
41
  PARAM_HUMIDITY_CURRENT,
42
+ PARAM_INPUT_REF,
35
43
  PARAM_LITELEC,
36
44
  PARAM_MODE,
37
45
  PARAM_MODES,
@@ -47,6 +55,8 @@ from .const import (
47
55
  PARAM_REF_TYPE,
48
56
  PARAM_RESTART_DEVICE,
49
57
  PARAM_SERVICES,
58
+ PARAM_SIREN_START,
59
+ PARAM_SIREN_STOP,
50
60
  PARAM_STATE,
51
61
  PARAM_STATUS,
52
62
  PARAM_STORAGE_USED,
@@ -54,6 +64,8 @@ from .const import (
54
64
  PARAM_STREAMS,
55
65
  PARAM_TEMPERATURE_CURRENT,
56
66
  PARAM_TOTAL_BYTES,
67
+ PARAM_TURN_INPUT_REF,
68
+ PARAM_TURN_REF,
57
69
  PARAM_URL,
58
70
  PARAM_USED_BYTES,
59
71
  PARAM_VALUE_TYPE,
@@ -68,6 +80,7 @@ from .const import (
68
80
  from .device import ImouDevice, ImouDeviceManager
69
81
  from .exceptions import RequestFailedException
70
82
  from .sensor import apply_sensor_state
83
+ from .siren import build_siren_start_iot_content
71
84
 
72
85
  _LOGGER: logging.Logger = logging.getLogger(__package__)
73
86
 
@@ -155,6 +168,7 @@ class ImouHaDevice:
155
168
  self._texts = {}
156
169
  self._channel_id = None
157
170
  self._channel_name = None
171
+ self._is_ipc = False
158
172
  self._product_id = None
159
173
  self._parent_product_id = None
160
174
  self._parent_device_id = None
@@ -171,6 +185,10 @@ class ImouHaDevice:
171
185
  def channel_name(self):
172
186
  return self._channel_name
173
187
 
188
+ @property
189
+ def is_ipc(self) -> bool:
190
+ return self._is_ipc
191
+
174
192
  @property
175
193
  def manufacturer(self):
176
194
  return self._manufacturer
@@ -245,6 +263,9 @@ class ImouHaDevice:
245
263
  def set_channel_name(self, channel_name):
246
264
  self._channel_name = channel_name
247
265
 
266
+ def set_is_ipc(self, is_ipc: bool) -> None:
267
+ self._is_ipc = is_ipc
268
+
248
269
 
249
270
  class ImouHaDeviceManager:
250
271
  def __init__(self, device_manager: ImouDeviceManager):
@@ -454,13 +475,79 @@ class ImouHaDeviceManager:
454
475
  async def _async_update_device_select_status(self, device: ImouHaDevice):
455
476
  """UPDATE SELECT STATUS"""
456
477
  for select_type, value in device.selects.items():
457
- if PARAM_REF in value:
478
+ if select_type == PARAM_COLLECTION_POINT:
479
+ await self._async_update_device_collection_points(device)
480
+ elif PARAM_REF in value:
458
481
  continue
459
482
  else:
460
483
  await self._async_update_device_select_status_by_type(
461
484
  device, select_type
462
485
  )
463
486
 
487
+ async def _async_update_device_collection_points(self, device: ImouHaDevice):
488
+ """Refresh collection point preset names."""
489
+ if device.channel_id is None:
490
+ return
491
+ try:
492
+ names: list[str]
493
+ select_state = device.selects[PARAM_COLLECTION_POINT]
494
+ if select_state.get(PARAM_TURN_REF):
495
+ output = await self._get_state_from_properties_or_services(
496
+ device,
497
+ self._resolve_device_id(device),
498
+ select_state,
499
+ kind="select",
500
+ key=PARAM_COLLECTION_POINT,
501
+ )
502
+ names = parse_iot_collection_names(output) if output is not None else []
503
+ else:
504
+ data = await self.delegate.async_get_device_collection(
505
+ device.device_id, str(device.channel_id)
506
+ )
507
+ names = parse_paas_collection_names(data)
508
+ except Exception as err:
509
+ _LOGGER.warning(
510
+ "Failed to fetch collection points for %s: %s",
511
+ device.device_name,
512
+ err,
513
+ )
514
+ return
515
+
516
+ device.selects[PARAM_COLLECTION_POINT][PARAM_OPTIONS] = (
517
+ build_collection_point_options(names)
518
+ )
519
+ device.selects[PARAM_COLLECTION_POINT][PARAM_CURRENT_OPTION] = (
520
+ PARAM_COLLECTION_POINT_PROMPT
521
+ )
522
+
523
+ async def _async_select_collection_point_option(
524
+ self, device: ImouHaDevice, option: str
525
+ ) -> None:
526
+ if option == PARAM_COLLECTION_POINT_PROMPT:
527
+ return
528
+ if device.channel_id is None:
529
+ raise RequestFailedException("collection_point requires channel")
530
+
531
+ select_state = device.selects[PARAM_COLLECTION_POINT]
532
+ turn_ref = select_state.get(PARAM_TURN_REF)
533
+ if turn_ref:
534
+ if device.product_id is None:
535
+ raise RequestFailedException("collection_point iot requires product_id")
536
+ input_ref = select_state.get(PARAM_TURN_INPUT_REF)
537
+ if not input_ref:
538
+ raise RequestFailedException("collection_point missing turn input ref")
539
+ await self.delegate.async_iot_device_control(
540
+ self._resolve_device_id(device),
541
+ device.product_id,
542
+ turn_ref,
543
+ {input_ref: option},
544
+ )
545
+ else:
546
+ await self.delegate.async_turn_device_collection(
547
+ device.device_id, str(device.channel_id), option
548
+ )
549
+ select_state[PARAM_CURRENT_OPTION] = PARAM_COLLECTION_POINT_PROMPT
550
+
464
551
  async def _async_update_device_sensor_status(self, device: ImouHaDevice):
465
552
  """UPDATE SENSOR STATUS"""
466
553
  for sensor_type, value in device.sensors.items():
@@ -651,6 +738,7 @@ class ImouHaDeviceManager:
651
738
  imou_ha_device.set_parent_product_id(device.parent_product_id)
652
739
  if device.parent_device_id is not None:
653
740
  imou_ha_device.set_parent_device_id(device.parent_device_id)
741
+ imou_ha_device.set_is_ipc(device.is_ipc)
654
742
  return imou_ha_device
655
743
 
656
744
  async def async_press_button(
@@ -665,10 +753,37 @@ class ImouHaDeviceManager:
665
753
  BUTTON_TYPE_PARAM_VALUE[button_type],
666
754
  duration,
667
755
  )
756
+ elif button_type in (PARAM_SIREN_START, PARAM_SIREN_STOP):
757
+ await self._async_press_siren_button(device, button_type)
668
758
  elif device.buttons[button_type].get(PARAM_REF):
669
759
  ref_id = device.buttons[button_type].get(PARAM_REF)
670
760
  await self._async_press_button_by_ref(device, ref_id)
671
761
 
762
+ async def _async_press_siren_button(
763
+ self, device: ImouHaDevice, button_type: str
764
+ ) -> None:
765
+ button_state = device.buttons[button_type]
766
+ ref = button_state.get(PARAM_REF)
767
+ if ref:
768
+ content: dict[str, str] = {}
769
+ if button_type == PARAM_SIREN_START:
770
+ input_ref = button_state.get(PARAM_INPUT_REF)
771
+ if not input_ref:
772
+ raise RequestFailedException("siren_start missing input ref")
773
+ content = build_siren_start_iot_content(input_ref)
774
+ await self._async_press_button_by_ref(device, ref, content)
775
+ return
776
+ if device.is_ipc:
777
+ channels = None
778
+ else:
779
+ if device.channel_id is None:
780
+ raise RequestFailedException(f"{button_type} requires channel")
781
+ channels = [int(device.channel_id)]
782
+ if button_type == PARAM_SIREN_START:
783
+ await self.delegate.async_siren_start(device.device_id, channels)
784
+ elif button_type == PARAM_SIREN_STOP:
785
+ await self.delegate.async_siren_stop(device.device_id, channels)
786
+
672
787
  async def async_set_text_value(
673
788
  self, device: ImouHaDevice, text_type: str, text_value: str
674
789
  ):
@@ -736,6 +851,7 @@ class ImouHaDeviceManager:
736
851
  # Request all failed, consider this operation a failure
737
852
  if all(isinstance(result_item, Exception) for result_item in result):
738
853
  raise result[0]
854
+ device.switches[switch_type][PARAM_STATE] = enable
739
855
 
740
856
  async def async_select_option(
741
857
  self,
@@ -743,17 +859,27 @@ class ImouHaDeviceManager:
743
859
  select_type: str,
744
860
  option: str,
745
861
  ):
862
+ if select_type == PARAM_COLLECTION_POINT:
863
+ await self._async_select_collection_point_option(device, option)
864
+ return
746
865
  if device.selects[select_type].get(PARAM_REF):
747
866
  ref_id = device.selects[select_type].get(PARAM_REF)
748
867
  value_type = device.selects[select_type].get(PARAM_VALUE_TYPE)
868
+ write_option = option
749
869
  # 兼容下音量15400值为-1的情况
750
870
  if ref_id == "15400" and option == "99":
751
- option = "-1"
752
- await self._async_select_option_by_ref(device, option, ref_id, value_type)
871
+ write_option = "-1"
872
+ await self._async_select_option_by_ref(
873
+ device, write_option, ref_id, value_type
874
+ )
875
+ device.selects[select_type][PARAM_CURRENT_OPTION] = option
753
876
  elif select_type == PARAM_NIGHT_VISION_MODE:
754
877
  await self.delegate.async_set_device_night_vision_mode(
755
878
  device.device_id, device.channel_id, option
756
879
  )
880
+ device.selects[PARAM_NIGHT_VISION_MODE][PARAM_CURRENT_OPTION] = (
881
+ option.lower()
882
+ )
757
883
 
758
884
  async def _async_get_device_switch_status_by_ability(
759
885
  self, device: ImouHaDevice, ability_type: str
@@ -886,10 +1012,16 @@ class ImouHaDeviceManager:
886
1012
  select_type,
887
1013
  imou_ha_device.selects,
888
1014
  ):
889
- imou_ha_device.selects[select_type] = {
890
- PARAM_CURRENT_OPTION: "",
891
- PARAM_OPTIONS: [],
892
- }
1015
+ if select_type == PARAM_COLLECTION_POINT:
1016
+ imou_ha_device.selects[select_type] = {
1017
+ PARAM_CURRENT_OPTION: PARAM_COLLECTION_POINT_PROMPT,
1018
+ PARAM_OPTIONS: [PARAM_COLLECTION_POINT_PROMPT],
1019
+ }
1020
+ else:
1021
+ imou_ha_device.selects[select_type] = {
1022
+ PARAM_CURRENT_OPTION: "",
1023
+ PARAM_OPTIONS: [],
1024
+ }
893
1025
 
894
1026
  @staticmethod
895
1027
  def configure_button_by_ability(
@@ -1083,9 +1215,10 @@ class ImouHaDeviceManager:
1083
1215
  imou_ha_device.product_id,
1084
1216
  ref.get(PARAM_EXCEPTS, []),
1085
1217
  ):
1086
- imou_ha_device.buttons[button_type] = {
1087
- PARAM_REF: ref[PARAM_REF],
1088
- }
1218
+ button_entry = {PARAM_REF: ref[PARAM_REF]}
1219
+ if ref.get(PARAM_INPUT_REF):
1220
+ button_entry[PARAM_INPUT_REF] = ref[PARAM_INPUT_REF]
1221
+ imou_ha_device.buttons[button_type] = button_entry
1089
1222
  break
1090
1223
 
1091
1224
  @staticmethod
@@ -1108,12 +1241,21 @@ class ImouHaDeviceManager:
1108
1241
  imou_ha_device.product_id,
1109
1242
  ref.get(PARAM_EXCEPTS, []),
1110
1243
  ):
1111
- imou_ha_device.selects[select_type] = {
1244
+ select_entry = {
1112
1245
  PARAM_REF: ref[PARAM_REF],
1113
1246
  PARAM_OPTIONS: ref[PARAM_OPTIONS],
1114
1247
  PARAM_CURRENT_OPTION: ref[PARAM_DEFAULT],
1115
1248
  PARAM_VALUE_TYPE: ref.get(PARAM_VALUE_TYPE, "str"),
1116
1249
  }
1250
+ if ref.get(PARAM_TURN_REF):
1251
+ select_entry[PARAM_TURN_REF] = ref[PARAM_TURN_REF]
1252
+ select_entry[PARAM_TURN_INPUT_REF] = ref.get(
1253
+ PARAM_TURN_INPUT_REF
1254
+ )
1255
+ select_entry[PARAM_REF_TYPE] = ref.get(
1256
+ PARAM_REF_TYPE, PARAM_PROPERTIES
1257
+ )
1258
+ imou_ha_device.selects[select_type] = select_entry
1117
1259
  break
1118
1260
 
1119
1261
  @staticmethod
@@ -1285,18 +1427,14 @@ class ImouHaDeviceManager:
1285
1427
  state = data
1286
1428
  return state
1287
1429
 
1288
- async def _async_press_button_by_ref(self, device: ImouHaDevice, ref: str):
1289
- device_id = device.device_id
1290
- if device.parent_product_id is not None:
1291
- device_id = (
1292
- device_id
1293
- + "_"
1294
- + device.parent_device_id
1295
- + "_"
1296
- + device.parent_product_id
1297
- )
1430
+ async def _async_press_button_by_ref(
1431
+ self, device: ImouHaDevice, ref: str, content: dict | None = None
1432
+ ):
1433
+ if content is None:
1434
+ content = {}
1435
+ device_id = self._resolve_device_id(device)
1298
1436
  await self.delegate.async_iot_device_control(
1299
- device_id, device.product_id, ref, {}
1437
+ device_id, device.product_id, ref, content
1300
1438
  )
1301
1439
 
1302
1440
  async def _async_select_option_by_ref(
@@ -1345,8 +1483,7 @@ class ImouHaDeviceManager:
1345
1483
  await self.delegate.async_set_iot_device_properties(
1346
1484
  device.device_id, None, device.product_id, {ref: 1 if enable else 0}
1347
1485
  )
1348
- await asyncio.sleep(3)
1349
- await self._async_update_device_switch_status_by_ref(device, switch_type, ref)
1486
+ device.switches[switch_type][PARAM_STATE] = enable
1350
1487
 
1351
1488
  @staticmethod
1352
1489
  def configure_binary_sensor_by_ability(
@@ -0,0 +1,15 @@
1
+ """Siren button helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+
7
+
8
+ def client_local_time_iso() -> str:
9
+ """Return client local time for IoT SirenStart (yyyyMMdd'T'HHmmss)."""
10
+ return datetime.now().astimezone().strftime("%Y%m%dT%H%M%S")
11
+
12
+
13
+ def build_siren_start_iot_content(input_ref: str) -> dict[str, str]:
14
+ """Build iotDeviceControl content for SirenStart."""
15
+ return {input_ref: client_local_time_iso()}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyimouapi
3
- Version: 1.3.2
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`.