pyimouapi 1.3.3__tar.gz → 1.3.4__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 (34) hide show
  1. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/PKG-INFO +1 -1
  2. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi/__init__.py +1 -1
  3. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi/const.py +21 -9
  4. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi/ha_device.py +15 -47
  5. pyimouapi-1.3.4/pyimouapi/select_option.py +123 -0
  6. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi.egg-info/PKG-INFO +1 -1
  7. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi.egg-info/SOURCES.txt +2 -0
  8. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyproject.toml +1 -1
  9. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/setup.py +1 -1
  10. pyimouapi-1.3.4/tests/test_select_option.py +111 -0
  11. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/tests/test_update_from_detail.py +6 -2
  12. pyimouapi-1.3.4/tests/test_write_optimistic_state.py +127 -0
  13. pyimouapi-1.3.3/tests/test_write_optimistic_state.py +0 -60
  14. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/LICENSE +0 -0
  15. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/README.md +0 -0
  16. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi/collection_point.py +0 -0
  17. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi/device.py +0 -0
  18. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi/exceptions.py +0 -0
  19. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi/openapi.py +0 -0
  20. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi/sensor.py +0 -0
  21. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi/siren.py +0 -0
  22. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi.egg-info/dependency_links.txt +0 -0
  23. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi.egg-info/requires.txt +0 -0
  24. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/pyimouapi.egg-info/top_level.txt +0 -0
  25. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/setup.cfg +0 -0
  26. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/tests/test_channel_id_and_api_guards.py +0 -0
  27. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/tests/test_collect_property_entities.py +0 -0
  28. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/tests/test_collection_point.py +0 -0
  29. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/tests/test_device_summaries.py +0 -0
  30. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/tests/test_event_identifier_map.py +0 -0
  31. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/tests/test_lookup_property.py +0 -0
  32. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/tests/test_message_callback.py +0 -0
  33. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/tests/test_sensor_normalize.py +0 -0
  34. {pyimouapi-1.3.3 → pyimouapi-1.3.4}/tests/test_siren.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyimouapi
3
- Version: 1.3.3
3
+ Version: 1.3.4
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.3"
1
+ __version__ = "1.3.4"
2
2
 
3
3
  from .device import ImouChannel, ImouDevice, ImouDeviceManager
4
4
  from .exceptions import (
@@ -113,6 +113,7 @@ PARAM_MOTION_DETECT = "motion_detect"
113
113
  PARAM_STORAGE_USED = "storage_used"
114
114
  PARAM_RESTART_DEVICE = "restart_device"
115
115
  PARAM_NIGHT_VISION_MODE = "night_vision_mode"
116
+ PARAM_DEVICE_VOLUME = "device_volume"
116
117
  PARAM_COLLECTION_POINT = "collection_point"
117
118
  PARAM_COLLECTION_POINT_PROMPT = "select_collection_point"
118
119
  PARAM_SIREN_START = "siren_start"
@@ -408,31 +409,42 @@ SELECT_TYPE_REF = {
408
409
  "night_vision_mode": [
409
410
  {
410
411
  "ref": "17400",
411
- "default": "0",
412
- "options": ["0", "1", "2", "3"],
412
+ "default": "intelligent",
413
+ "options": ["intelligent", "fullcolor", "infrared", "off"],
413
414
  "value_type": "int",
414
415
  },
415
416
  {
416
417
  "ref": "139700",
417
- "default": "0",
418
- "options": ["0", "1", "2", "3", "4"],
418
+ "default": "intelligent",
419
+ "options": [
420
+ "intelligent",
421
+ "fullcolor",
422
+ "infrared",
423
+ "off",
424
+ "custom",
425
+ ],
426
+ "value_type": "int",
427
+ },
428
+ {
429
+ "ref": "112400",
430
+ "default": "infrared",
431
+ "options": ["infrared", "off"],
419
432
  "value_type": "int",
420
433
  },
421
- {"ref": "112400", "default": "2", "options": ["2", "3"], "value_type": "int"},
422
434
  ],
423
435
  "mode": [
424
436
  {
425
437
  "ref": "15200",
426
- "default": "0",
427
- "options": ["0", "1", "2"],
438
+ "default": "home",
439
+ "options": ["home", "away", "disarm"],
428
440
  "value_type": "int",
429
441
  }
430
442
  ],
431
443
  "device_volume": [
432
444
  {
433
445
  "ref": "15400",
434
- "default": "0",
435
- "options": ["99", "0", "1", "2"],
446
+ "default": "low",
447
+ "options": ["mute", "low", "medium", "high"],
436
448
  "value_type": "int",
437
449
  }
438
450
  ],
@@ -79,6 +79,7 @@ from .const import (
79
79
  )
80
80
  from .device import ImouDevice, ImouDeviceManager
81
81
  from .exceptions import RequestFailedException
82
+ from .select_option import normalize_options, to_friendly, to_raw
82
83
  from .sensor import apply_sensor_state
83
84
  from .siren import build_siren_start_iot_content
84
85
 
@@ -358,16 +359,13 @@ class ImouHaDeviceManager:
358
359
  meta: dict[str, Any],
359
360
  raw_value: Any,
360
361
  ) -> None:
361
- ref = meta[PARAM_REF]
362
362
  if kind == "switch":
363
363
  device.switches[key][PARAM_STATE] = raw_value == 1
364
364
  elif kind == "binary_sensor":
365
365
  device.binary_sensors[key][PARAM_STATE] = raw_value == 1
366
366
  elif kind == "select":
367
367
  value = str(raw_value) if isinstance(raw_value, int) else raw_value
368
- device.selects[key][PARAM_CURRENT_OPTION] = value
369
- if ref == "15400" and value == "-1":
370
- device.selects[key][PARAM_CURRENT_OPTION] = "99"
368
+ device.selects[key][PARAM_CURRENT_OPTION] = to_friendly(key, value)
371
369
  elif kind in ("sensor", "text"):
372
370
  if meta.get(PARAM_EXPRESSION) and isinstance(raw_value, dict | list):
373
371
  state = self.get_expression_value(meta[PARAM_EXPRESSION], raw_value)
@@ -819,6 +817,7 @@ class ImouHaDeviceManager:
819
817
  await self.delegate.async_set_iot_device_properties(
820
818
  device.device_id, None, device.product_id, {ref_id: value}
821
819
  )
820
+ device.texts[text_type][PARAM_STATE] = str(text_value)
822
821
 
823
822
  async def async_switch_operation(
824
823
  self, device: ImouHaDevice, switch_type: str, enable: bool
@@ -865,9 +864,9 @@ class ImouHaDeviceManager:
865
864
  if device.selects[select_type].get(PARAM_REF):
866
865
  ref_id = device.selects[select_type].get(PARAM_REF)
867
866
  value_type = device.selects[select_type].get(PARAM_VALUE_TYPE)
868
- write_option = option
867
+ write_option = to_raw(select_type, option)
869
868
  # 兼容下音量15400值为-1的情况
870
- if ref_id == "15400" and option == "99":
869
+ if ref_id == "15400" and write_option == "99":
871
870
  write_option = "-1"
872
871
  await self._async_select_option_by_ref(
873
872
  device, write_option, ref_id, value_type
@@ -877,8 +876,8 @@ class ImouHaDeviceManager:
877
876
  await self.delegate.async_set_device_night_vision_mode(
878
877
  device.device_id, device.channel_id, option
879
878
  )
880
- device.selects[PARAM_NIGHT_VISION_MODE][PARAM_CURRENT_OPTION] = (
881
- option.lower()
879
+ device.selects[PARAM_NIGHT_VISION_MODE][PARAM_CURRENT_OPTION] = to_friendly(
880
+ PARAM_NIGHT_VISION_MODE, option
882
881
  )
883
882
 
884
883
  async def _async_get_device_switch_status_by_ability(
@@ -922,13 +921,14 @@ class ImouHaDeviceManager:
922
921
  if PARAM_MODE not in data or PARAM_MODES not in data:
923
922
  raise RequestFailedException("get_device_night_vision fail")
924
923
  if data[PARAM_MODE] is not None:
925
- device.selects[PARAM_NIGHT_VISION_MODE][PARAM_CURRENT_OPTION] = data[
926
- PARAM_MODE
927
- ].lower()
924
+ device.selects[PARAM_NIGHT_VISION_MODE][PARAM_CURRENT_OPTION] = to_friendly(
925
+ PARAM_NIGHT_VISION_MODE, data[PARAM_MODE]
926
+ )
928
927
  if data[PARAM_MODES] is not None:
929
- device.selects[PARAM_NIGHT_VISION_MODE][PARAM_OPTIONS] = [
930
- item.lower() for item in data[PARAM_MODES]
931
- ]
928
+ device.selects[PARAM_NIGHT_VISION_MODE][PARAM_OPTIONS] = normalize_options(
929
+ PARAM_NIGHT_VISION_MODE,
930
+ [item.lower() for item in data[PARAM_MODES]],
931
+ )
932
932
 
933
933
  @staticmethod
934
934
  def configure_device_by_ability(
@@ -1337,34 +1337,6 @@ class ImouHaDeviceManager:
1337
1337
  except Exception as e:
1338
1338
  _LOGGER.error(f"_async_update_device_switch_status_by_ref fail:{e}")
1339
1339
 
1340
- async def _async_update_device_select_status_by_ref(
1341
- self, device: ImouHaDevice, select_type: str, ref: str
1342
- ):
1343
- try:
1344
- device_id = self._resolve_device_id(device)
1345
- data = await self.delegate.async_get_iot_device_properties(
1346
- device_id, device.channel_id, device.product_id, [ref]
1347
- )
1348
- if ref in data[PARAM_PROPERTIES]:
1349
- value = (
1350
- str(data[PARAM_PROPERTIES][ref])
1351
- if isinstance(data[PARAM_PROPERTIES][ref], int)
1352
- else data[PARAM_PROPERTIES][ref]
1353
- )
1354
- device.selects[select_type][PARAM_CURRENT_OPTION] = value
1355
- if ref == "15400" and value == "-1":
1356
- device.selects[select_type][PARAM_CURRENT_OPTION] = "99"
1357
- else:
1358
- self._debug_missing_property_ref(
1359
- device,
1360
- ref,
1361
- source="getIotDeviceProperties",
1362
- kind="select",
1363
- key=select_type,
1364
- )
1365
- except Exception as e:
1366
- _LOGGER.error(f"Error while updating device select status: {e}")
1367
-
1368
1340
  async def _async_update_device_sensor_status_by_ref(
1369
1341
  self,
1370
1342
  device: ImouHaDevice,
@@ -1661,11 +1633,7 @@ class ImouHaDeviceManager:
1661
1633
  await self.delegate.async_iot_device_control(
1662
1634
  device_id, device.product_id, "28600", param
1663
1635
  )
1664
- # 等待1秒,查询倒计时
1665
- await asyncio.sleep(1)
1666
- await self._async_update_device_text_status_by_ref(
1667
- device, "count_down_switch", device.texts["count_down_switch"]
1668
- )
1636
+ device.texts["count_down_switch"][PARAM_STATE] = str(text_value)
1669
1637
 
1670
1638
 
1671
1639
  class DeviceStatus(Enum):
@@ -0,0 +1,123 @@
1
+ """Select option key normalization for Home Assistant integrations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from pyimouapi.const import (
9
+ PARAM_COLLECTION_POINT,
10
+ PARAM_DEVICE_VOLUME,
11
+ PARAM_MODE,
12
+ PARAM_NIGHT_VISION_MODE,
13
+ )
14
+
15
+ _LOGGER = logging.getLogger(__name__)
16
+
17
+ MAPPED_SELECT_TYPES = frozenset(
18
+ {PARAM_MODE, PARAM_DEVICE_VOLUME, PARAM_NIGHT_VISION_MODE}
19
+ )
20
+
21
+ # raw string -> friendly
22
+ _VOLUME_RAW_TO_FRIENDLY = {
23
+ "99": "mute",
24
+ "-1": "mute",
25
+ "0": "low",
26
+ "1": "medium",
27
+ "2": "high",
28
+ }
29
+ _VOLUME_FRIENDLY_TO_RAW = {
30
+ "mute": "99",
31
+ "low": "0",
32
+ "medium": "1",
33
+ "high": "2",
34
+ }
35
+
36
+ _MODE_RAW_TO_FRIENDLY = {"0": "home", "1": "away", "2": "disarm"}
37
+ _MODE_FRIENDLY_TO_RAW = {v: k for k, v in _MODE_RAW_TO_FRIENDLY.items()}
38
+
39
+ _NIGHT_VISION_RAW_TO_FRIENDLY = {
40
+ "0": "intelligent",
41
+ "1": "fullcolor",
42
+ "2": "infrared",
43
+ "3": "off",
44
+ "4": "custom",
45
+ }
46
+ _NIGHT_VISION_FRIENDLY_TO_RAW = {
47
+ "intelligent": "0",
48
+ "fullcolor": "1",
49
+ "infrared": "2",
50
+ "off": "3",
51
+ "custom": "4",
52
+ "lowlight": "lowlight",
53
+ "smartlowlight": "smartlowlight",
54
+ }
55
+ # PaaS already-friendly keys also accepted as raw (identity after lower)
56
+ for _k in (
57
+ "intelligent",
58
+ "fullcolor",
59
+ "infrared",
60
+ "off",
61
+ "custom",
62
+ "lowlight",
63
+ "smartlowlight",
64
+ ):
65
+ _NIGHT_VISION_RAW_TO_FRIENDLY.setdefault(_k, _k)
66
+
67
+
68
+ def _as_raw_str(raw: Any) -> str:
69
+ if raw is None:
70
+ return ""
71
+ return str(raw).strip()
72
+
73
+
74
+ def to_friendly(select_type: str, raw: Any) -> str:
75
+ """Map vendor/API raw option to a stable friendly key."""
76
+ if select_type == PARAM_COLLECTION_POINT or select_type not in MAPPED_SELECT_TYPES:
77
+ return _as_raw_str(raw)
78
+
79
+ key = _as_raw_str(raw)
80
+ if select_type == PARAM_DEVICE_VOLUME:
81
+ table = _VOLUME_RAW_TO_FRIENDLY
82
+ elif select_type == PARAM_MODE:
83
+ table = _MODE_RAW_TO_FRIENDLY
84
+ else:
85
+ key_l = key.lower()
86
+ mapped = _NIGHT_VISION_RAW_TO_FRIENDLY.get(key_l)
87
+ if mapped is not None:
88
+ return mapped
89
+ _LOGGER.debug("unknown night_vision raw option %r", raw)
90
+ return key_l
91
+
92
+ if key in table:
93
+ return table[key]
94
+ _LOGGER.debug("unknown %s raw option %r", select_type, raw)
95
+ return key
96
+
97
+
98
+ def to_raw(select_type: str, friendly: str) -> str:
99
+ """Map friendly key to vendor/API raw string for IoT writes.
100
+
101
+ For PaaS-only night vision keys (`lowlight`, `smartlowlight`), returns the
102
+ same lowercase string (caller uses NIGHT_VISION_MODE_MAP).
103
+ Raises ValueError for unknown keys on mapped select types.
104
+ """
105
+ if select_type == PARAM_COLLECTION_POINT or select_type not in MAPPED_SELECT_TYPES:
106
+ return friendly
107
+
108
+ key = friendly.strip().lower()
109
+ if select_type == PARAM_DEVICE_VOLUME:
110
+ table = _VOLUME_FRIENDLY_TO_RAW
111
+ elif select_type == PARAM_MODE:
112
+ table = _MODE_FRIENDLY_TO_RAW
113
+ else:
114
+ table = _NIGHT_VISION_FRIENDLY_TO_RAW
115
+
116
+ if key not in table:
117
+ raise ValueError(f"unknown {select_type} option: {friendly!r}")
118
+ return table[key]
119
+
120
+
121
+ def normalize_options(select_type: str, options: list[str]) -> list[str]:
122
+ """Return options list with each entry passed through to_friendly."""
123
+ return [to_friendly(select_type, item) for item in options]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyimouapi
3
- Version: 1.3.3
3
+ Version: 1.3.4
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
@@ -9,6 +9,7 @@ pyimouapi/device.py
9
9
  pyimouapi/exceptions.py
10
10
  pyimouapi/ha_device.py
11
11
  pyimouapi/openapi.py
12
+ pyimouapi/select_option.py
12
13
  pyimouapi/sensor.py
13
14
  pyimouapi/siren.py
14
15
  pyimouapi.egg-info/PKG-INFO
@@ -23,6 +24,7 @@ tests/test_device_summaries.py
23
24
  tests/test_event_identifier_map.py
24
25
  tests/test_lookup_property.py
25
26
  tests/test_message_callback.py
27
+ tests/test_select_option.py
26
28
  tests/test_sensor_normalize.py
27
29
  tests/test_siren.py
28
30
  tests/test_update_from_detail.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyimouapi"
7
- version = "1.3.3"
7
+ version = "1.3.4"
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.3",
7
+ version="1.3.4",
8
8
  packages=find_packages(),
9
9
  python_requires=">=3.11",
10
10
  install_requires=[
@@ -0,0 +1,111 @@
1
+ """Tests for select option friendly-key normalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ from pyimouapi.const import PARAM_DEVICE_VOLUME, PARAM_MODE, PARAM_NIGHT_VISION_MODE
7
+ from pyimouapi.ha_device import ImouHaDevice, ImouHaDeviceManager
8
+ from pyimouapi.select_option import normalize_options, to_friendly, to_raw
9
+
10
+
11
+ @pytest.mark.parametrize(
12
+ ("select_type", "raw", "friendly"),
13
+ [
14
+ (PARAM_DEVICE_VOLUME, "99", "mute"),
15
+ (PARAM_DEVICE_VOLUME, "-1", "mute"),
16
+ (PARAM_DEVICE_VOLUME, -1, "mute"),
17
+ (PARAM_DEVICE_VOLUME, "0", "low"),
18
+ (PARAM_DEVICE_VOLUME, "1", "medium"),
19
+ (PARAM_DEVICE_VOLUME, "2", "high"),
20
+ (PARAM_MODE, "0", "home"),
21
+ (PARAM_MODE, "1", "away"),
22
+ (PARAM_MODE, "2", "disarm"),
23
+ (PARAM_NIGHT_VISION_MODE, "0", "intelligent"),
24
+ (PARAM_NIGHT_VISION_MODE, "1", "fullcolor"),
25
+ (PARAM_NIGHT_VISION_MODE, "2", "infrared"),
26
+ (PARAM_NIGHT_VISION_MODE, "3", "off"),
27
+ (PARAM_NIGHT_VISION_MODE, "4", "custom"),
28
+ (PARAM_NIGHT_VISION_MODE, "intelligent", "intelligent"),
29
+ (PARAM_NIGHT_VISION_MODE, "FullColor", "fullcolor"),
30
+ (PARAM_NIGHT_VISION_MODE, "lowlight", "lowlight"),
31
+ (PARAM_NIGHT_VISION_MODE, "smartlowlight", "smartlowlight"),
32
+ ],
33
+ )
34
+ def test_to_friendly(select_type: str, raw: str | int, friendly: str) -> None:
35
+ assert to_friendly(select_type, raw) == friendly
36
+
37
+
38
+ @pytest.mark.parametrize(
39
+ ("select_type", "friendly", "raw"),
40
+ [
41
+ (PARAM_DEVICE_VOLUME, "mute", "99"),
42
+ (PARAM_DEVICE_VOLUME, "low", "0"),
43
+ (PARAM_DEVICE_VOLUME, "medium", "1"),
44
+ (PARAM_DEVICE_VOLUME, "high", "2"),
45
+ (PARAM_MODE, "home", "0"),
46
+ (PARAM_MODE, "away", "1"),
47
+ (PARAM_MODE, "disarm", "2"),
48
+ (PARAM_NIGHT_VISION_MODE, "intelligent", "0"),
49
+ (PARAM_NIGHT_VISION_MODE, "fullcolor", "1"),
50
+ (PARAM_NIGHT_VISION_MODE, "infrared", "2"),
51
+ (PARAM_NIGHT_VISION_MODE, "off", "3"),
52
+ (PARAM_NIGHT_VISION_MODE, "custom", "4"),
53
+ (PARAM_NIGHT_VISION_MODE, "lowlight", "lowlight"),
54
+ (PARAM_NIGHT_VISION_MODE, "smartlowlight", "smartlowlight"),
55
+ ],
56
+ )
57
+ def test_to_raw(select_type: str, friendly: str, raw: str) -> None:
58
+ assert to_raw(select_type, friendly) == raw
59
+
60
+
61
+ def test_to_friendly_unknown_raw_passthrough() -> None:
62
+ assert to_friendly(PARAM_MODE, "9") == "9"
63
+
64
+
65
+ def test_to_raw_unknown_friendly_raises() -> None:
66
+ with pytest.raises(ValueError, match="unknown"):
67
+ to_raw(PARAM_MODE, "vacation")
68
+
69
+
70
+ def test_normalize_options_maps_list() -> None:
71
+ assert normalize_options(PARAM_MODE, ["0", "1", "2"]) == [
72
+ "home",
73
+ "away",
74
+ "disarm",
75
+ ]
76
+
77
+
78
+ def test_collection_point_identity() -> None:
79
+ from pyimouapi.const import PARAM_COLLECTION_POINT
80
+
81
+ assert to_friendly(PARAM_COLLECTION_POINT, "door") == "door"
82
+ assert to_raw(PARAM_COLLECTION_POINT, "door") == "door"
83
+
84
+
85
+ def test_configure_select_by_ref_uses_friendly_defaults() -> None:
86
+ device = ImouHaDevice("d1", "cam", "Imou", "IPC", "1.0")
87
+ device.set_channel_id("0")
88
+ device.set_product_id("pid")
89
+ # refs that match SELECT_TYPE_REF entries
90
+ ImouHaDeviceManager.configure_select_by_ref(
91
+ ["15200", "15400", "17400"],
92
+ True,
93
+ [],
94
+ device,
95
+ )
96
+ assert device.selects[PARAM_MODE]["options"] == ["home", "away", "disarm"]
97
+ assert device.selects[PARAM_MODE]["current_option"] == "home"
98
+ assert device.selects[PARAM_DEVICE_VOLUME]["options"] == [
99
+ "mute",
100
+ "low",
101
+ "medium",
102
+ "high",
103
+ ]
104
+ assert device.selects[PARAM_DEVICE_VOLUME]["current_option"] == "low"
105
+ assert device.selects[PARAM_NIGHT_VISION_MODE]["options"] == [
106
+ "intelligent",
107
+ "fullcolor",
108
+ "infrared",
109
+ "off",
110
+ ]
111
+ assert device.selects[PARAM_NIGHT_VISION_MODE]["current_option"] == "intelligent"
@@ -6,6 +6,7 @@ import pytest
6
6
  from pyimouapi.const import (
7
7
  PARAM_CHANNELS,
8
8
  PARAM_CURRENT_OPTION,
9
+ PARAM_DEVICE_VOLUME,
9
10
  PARAM_ONLINE,
10
11
  PARAM_PROPERTIES,
11
12
  PARAM_REF,
@@ -26,7 +27,10 @@ def _online_device() -> ImouHaDevice:
26
27
  async def test_update_from_detail_applies_switch_and_select():
27
28
  device = _online_device()
28
29
  device.switches["relay"] = {PARAM_REF: "10001", PARAM_STATE: False}
29
- device.selects["volume"] = {PARAM_REF: "15400", PARAM_CURRENT_OPTION: "0"}
30
+ device.selects[PARAM_DEVICE_VOLUME] = {
31
+ PARAM_REF: "15400",
32
+ PARAM_CURRENT_OPTION: "low",
33
+ }
30
34
 
31
35
  detail = {
32
36
  PARAM_PROPERTIES: {"10001": 1, "15400": -1},
@@ -39,7 +43,7 @@ async def test_update_from_detail_applies_switch_and_select():
39
43
  await manager._async_update_properties_from_detail(device, detail)
40
44
 
41
45
  assert device.switches["relay"][PARAM_STATE] is True
42
- assert device.selects["volume"][PARAM_CURRENT_OPTION] == "99"
46
+ assert device.selects[PARAM_DEVICE_VOLUME][PARAM_CURRENT_OPTION] == "mute"
43
47
 
44
48
 
45
49
  @pytest.mark.asyncio
@@ -0,0 +1,127 @@
1
+ """Tests for optimistic local state after writes (no post-write cloud read)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import AsyncMock, MagicMock
6
+
7
+ import pytest
8
+ from pyimouapi.const import (
9
+ PARAM_CURRENT_OPTION,
10
+ PARAM_DEVICE_VOLUME,
11
+ PARAM_MODE,
12
+ PARAM_OPTIONS,
13
+ PARAM_REF,
14
+ PARAM_STATE,
15
+ PARAM_VALUE_TYPE,
16
+ )
17
+ from pyimouapi.ha_device import ImouHaDevice, ImouHaDeviceManager
18
+
19
+
20
+ def _ha_device(*, product_id: str | None = "prod1") -> ImouHaDevice:
21
+ device = ImouHaDevice("DEV001", "Camera", "Imou", "IPC", "1.0")
22
+ device.set_channel_id("0")
23
+ if product_id is not None:
24
+ device.set_product_id(product_id)
25
+ return device
26
+
27
+
28
+ @pytest.mark.asyncio
29
+ async def test_switch_operation_by_ref_updates_local_state_without_read() -> None:
30
+ """IoT switch writes update local state without getIotDeviceProperties."""
31
+ device = _ha_device()
32
+ device.switches["motion_detect"] = {PARAM_REF: "12300", PARAM_STATE: False}
33
+ delegate = MagicMock()
34
+ delegate.async_set_iot_device_properties = AsyncMock()
35
+ delegate.async_get_iot_device_properties = AsyncMock()
36
+ manager = ImouHaDeviceManager(delegate)
37
+
38
+ await manager.async_switch_operation(device, "motion_detect", True)
39
+
40
+ assert device.switches["motion_detect"][PARAM_STATE] is True
41
+ delegate.async_get_iot_device_properties.assert_not_called()
42
+
43
+
44
+ @pytest.mark.asyncio
45
+ async def test_select_option_by_ref_updates_local_state_without_read() -> None:
46
+ """IoT select writes update current_option without a coordinator refresh read."""
47
+ device = _ha_device()
48
+ device.selects[PARAM_MODE] = {
49
+ PARAM_REF: "15200",
50
+ PARAM_CURRENT_OPTION: "home",
51
+ PARAM_OPTIONS: ["home", "away", "disarm"],
52
+ PARAM_VALUE_TYPE: "int",
53
+ }
54
+ delegate = MagicMock()
55
+ delegate.async_set_iot_device_properties = AsyncMock()
56
+ delegate.async_get_iot_device_properties = AsyncMock()
57
+ manager = ImouHaDeviceManager(delegate)
58
+
59
+ await manager.async_select_option(device, PARAM_MODE, "away")
60
+
61
+ assert device.selects[PARAM_MODE][PARAM_CURRENT_OPTION] == "away"
62
+ props = delegate.async_set_iot_device_properties.await_args.args[3]
63
+ assert props == {"15200": 1}
64
+ delegate.async_get_iot_device_properties.assert_not_called()
65
+
66
+
67
+ @pytest.mark.asyncio
68
+ async def test_select_volume_mute_writes_minus_one() -> None:
69
+ device = _ha_device()
70
+ device.selects[PARAM_DEVICE_VOLUME] = {
71
+ PARAM_REF: "15400",
72
+ PARAM_CURRENT_OPTION: "low",
73
+ PARAM_OPTIONS: ["mute", "low", "medium", "high"],
74
+ PARAM_VALUE_TYPE: "int",
75
+ }
76
+ delegate = MagicMock()
77
+ delegate.async_set_iot_device_properties = AsyncMock()
78
+ manager = ImouHaDeviceManager(delegate)
79
+
80
+ await manager.async_select_option(device, PARAM_DEVICE_VOLUME, "mute")
81
+
82
+ assert device.selects[PARAM_DEVICE_VOLUME][PARAM_CURRENT_OPTION] == "mute"
83
+ props = delegate.async_set_iot_device_properties.await_args.args[3]
84
+ assert props == {"15400": -1}
85
+
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_set_text_value_by_ref_updates_local_state_without_read() -> None:
89
+ """IoT text writes update local state without getIotDeviceProperties."""
90
+ device = _ha_device()
91
+ device.texts["overcharge_switch"] = {
92
+ PARAM_REF: "128900",
93
+ PARAM_STATE: "5",
94
+ PARAM_VALUE_TYPE: "int",
95
+ }
96
+ delegate = MagicMock()
97
+ delegate.async_set_iot_device_properties = AsyncMock()
98
+ delegate.async_get_iot_device_properties = AsyncMock()
99
+ manager = ImouHaDeviceManager(delegate)
100
+
101
+ await manager.async_set_text_value(device, "overcharge_switch", "100")
102
+
103
+ assert device.texts["overcharge_switch"][PARAM_STATE] == "100"
104
+ delegate.async_get_iot_device_properties.assert_not_called()
105
+
106
+
107
+ @pytest.mark.asyncio
108
+ async def test_set_count_down_text_updates_local_state_without_read() -> None:
109
+ """Countdown text writes update local state without sleep/re-query."""
110
+ device = _ha_device()
111
+ device.switches["switch"] = {PARAM_REF: "10000", PARAM_STATE: False}
112
+ device.texts["count_down_switch"] = {
113
+ PARAM_REF: "28800",
114
+ PARAM_STATE: "0",
115
+ }
116
+ delegate = MagicMock()
117
+ delegate.async_iot_device_control = AsyncMock()
118
+ delegate.async_get_iot_device_properties = AsyncMock()
119
+ manager = ImouHaDeviceManager(delegate)
120
+ manager._async_update_device_switch_status_by_ref = AsyncMock()
121
+
122
+ await manager.async_set_text_value(device, "count_down_switch", "10")
123
+
124
+ assert device.texts["count_down_switch"][PARAM_STATE] == "10"
125
+ manager._async_update_device_switch_status_by_ref.assert_awaited_once()
126
+ delegate.async_iot_device_control.assert_awaited_once()
127
+ delegate.async_get_iot_device_properties.assert_not_called()
@@ -1,60 +0,0 @@
1
- """Tests for optimistic local state after writes (no post-write cloud read)."""
2
-
3
- from __future__ import annotations
4
-
5
- from unittest.mock import AsyncMock, MagicMock
6
-
7
- import pytest
8
- from pyimouapi.const import (
9
- PARAM_CURRENT_OPTION,
10
- PARAM_MODE,
11
- PARAM_OPTIONS,
12
- PARAM_REF,
13
- PARAM_STATE,
14
- PARAM_VALUE_TYPE,
15
- )
16
- from pyimouapi.ha_device import ImouHaDevice, ImouHaDeviceManager
17
-
18
-
19
- def _ha_device(*, product_id: str | None = "prod1") -> ImouHaDevice:
20
- device = ImouHaDevice("DEV001", "Camera", "Imou", "IPC", "1.0")
21
- device.set_channel_id("0")
22
- if product_id is not None:
23
- device.set_product_id(product_id)
24
- return device
25
-
26
-
27
- @pytest.mark.asyncio
28
- async def test_switch_operation_by_ref_updates_local_state_without_read() -> None:
29
- """IoT switch writes update local state without getIotDeviceProperties."""
30
- device = _ha_device()
31
- device.switches["motion_detect"] = {PARAM_REF: "12300", PARAM_STATE: False}
32
- delegate = MagicMock()
33
- delegate.async_set_iot_device_properties = AsyncMock()
34
- delegate.async_get_iot_device_properties = AsyncMock()
35
- manager = ImouHaDeviceManager(delegate)
36
-
37
- await manager.async_switch_operation(device, "motion_detect", True)
38
-
39
- assert device.switches["motion_detect"][PARAM_STATE] is True
40
- delegate.async_get_iot_device_properties.assert_not_called()
41
-
42
-
43
- @pytest.mark.asyncio
44
- async def test_select_option_by_ref_updates_local_state_without_read() -> None:
45
- """IoT select writes update current_option without a coordinator refresh read."""
46
- device = _ha_device()
47
- device.selects[PARAM_MODE] = {
48
- PARAM_REF: "15200",
49
- PARAM_CURRENT_OPTION: "0",
50
- PARAM_OPTIONS: ["0", "1", "2"],
51
- PARAM_VALUE_TYPE: "int",
52
- }
53
- delegate = MagicMock()
54
- delegate.async_set_iot_device_properties = AsyncMock()
55
- manager = ImouHaDeviceManager(delegate)
56
-
57
- await manager.async_select_option(device, PARAM_MODE, "1")
58
-
59
- assert device.selects[PARAM_MODE][PARAM_CURRENT_OPTION] == "1"
60
- delegate.async_get_iot_device_properties.assert_not_called()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes