aiohomematic 2025.11.3__py3-none-any.whl

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.

Potentially problematic release.


This version of aiohomematic might be problematic. Click here for more details.

Files changed (77) hide show
  1. aiohomematic/__init__.py +61 -0
  2. aiohomematic/async_support.py +212 -0
  3. aiohomematic/central/__init__.py +2309 -0
  4. aiohomematic/central/decorators.py +155 -0
  5. aiohomematic/central/rpc_server.py +295 -0
  6. aiohomematic/client/__init__.py +1848 -0
  7. aiohomematic/client/_rpc_errors.py +81 -0
  8. aiohomematic/client/json_rpc.py +1326 -0
  9. aiohomematic/client/rpc_proxy.py +311 -0
  10. aiohomematic/const.py +1127 -0
  11. aiohomematic/context.py +18 -0
  12. aiohomematic/converter.py +108 -0
  13. aiohomematic/decorators.py +302 -0
  14. aiohomematic/exceptions.py +164 -0
  15. aiohomematic/hmcli.py +186 -0
  16. aiohomematic/model/__init__.py +140 -0
  17. aiohomematic/model/calculated/__init__.py +84 -0
  18. aiohomematic/model/calculated/climate.py +290 -0
  19. aiohomematic/model/calculated/data_point.py +327 -0
  20. aiohomematic/model/calculated/operating_voltage_level.py +299 -0
  21. aiohomematic/model/calculated/support.py +234 -0
  22. aiohomematic/model/custom/__init__.py +177 -0
  23. aiohomematic/model/custom/climate.py +1532 -0
  24. aiohomematic/model/custom/cover.py +792 -0
  25. aiohomematic/model/custom/data_point.py +334 -0
  26. aiohomematic/model/custom/definition.py +871 -0
  27. aiohomematic/model/custom/light.py +1128 -0
  28. aiohomematic/model/custom/lock.py +394 -0
  29. aiohomematic/model/custom/siren.py +275 -0
  30. aiohomematic/model/custom/support.py +41 -0
  31. aiohomematic/model/custom/switch.py +175 -0
  32. aiohomematic/model/custom/valve.py +114 -0
  33. aiohomematic/model/data_point.py +1123 -0
  34. aiohomematic/model/device.py +1445 -0
  35. aiohomematic/model/event.py +208 -0
  36. aiohomematic/model/generic/__init__.py +217 -0
  37. aiohomematic/model/generic/action.py +34 -0
  38. aiohomematic/model/generic/binary_sensor.py +30 -0
  39. aiohomematic/model/generic/button.py +27 -0
  40. aiohomematic/model/generic/data_point.py +171 -0
  41. aiohomematic/model/generic/dummy.py +147 -0
  42. aiohomematic/model/generic/number.py +76 -0
  43. aiohomematic/model/generic/select.py +39 -0
  44. aiohomematic/model/generic/sensor.py +74 -0
  45. aiohomematic/model/generic/switch.py +54 -0
  46. aiohomematic/model/generic/text.py +29 -0
  47. aiohomematic/model/hub/__init__.py +333 -0
  48. aiohomematic/model/hub/binary_sensor.py +24 -0
  49. aiohomematic/model/hub/button.py +28 -0
  50. aiohomematic/model/hub/data_point.py +340 -0
  51. aiohomematic/model/hub/number.py +39 -0
  52. aiohomematic/model/hub/select.py +49 -0
  53. aiohomematic/model/hub/sensor.py +37 -0
  54. aiohomematic/model/hub/switch.py +44 -0
  55. aiohomematic/model/hub/text.py +30 -0
  56. aiohomematic/model/support.py +586 -0
  57. aiohomematic/model/update.py +143 -0
  58. aiohomematic/property_decorators.py +496 -0
  59. aiohomematic/py.typed +0 -0
  60. aiohomematic/rega_scripts/fetch_all_device_data.fn +92 -0
  61. aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
  62. aiohomematic/rega_scripts/get_serial.fn +44 -0
  63. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
  64. aiohomematic/rega_scripts/set_program_state.fn +12 -0
  65. aiohomematic/rega_scripts/set_system_variable.fn +15 -0
  66. aiohomematic/store/__init__.py +34 -0
  67. aiohomematic/store/dynamic.py +551 -0
  68. aiohomematic/store/persistent.py +988 -0
  69. aiohomematic/store/visibility.py +812 -0
  70. aiohomematic/support.py +664 -0
  71. aiohomematic/validator.py +112 -0
  72. aiohomematic-2025.11.3.dist-info/METADATA +144 -0
  73. aiohomematic-2025.11.3.dist-info/RECORD +77 -0
  74. aiohomematic-2025.11.3.dist-info/WHEEL +5 -0
  75. aiohomematic-2025.11.3.dist-info/entry_points.txt +2 -0
  76. aiohomematic-2025.11.3.dist-info/licenses/LICENSE +21 -0
  77. aiohomematic-2025.11.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,394 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025
3
+ """Module for data points implemented using the lock category."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from abc import abstractmethod
8
+ from collections.abc import Mapping
9
+ from enum import StrEnum
10
+
11
+ from aiohomematic.const import DataPointCategory, DeviceProfile, Field, Parameter
12
+ from aiohomematic.model import device as hmd
13
+ from aiohomematic.model.custom import definition as hmed
14
+ from aiohomematic.model.custom.data_point import CustomDataPoint
15
+ from aiohomematic.model.custom.support import CustomConfig, ExtendedConfig
16
+ from aiohomematic.model.data_point import CallParameterCollector, bind_collector
17
+ from aiohomematic.model.generic import DpAction, DpSensor, DpSwitch
18
+ from aiohomematic.property_decorators import state_property
19
+
20
+
21
+ class _LockActivity(StrEnum):
22
+ """Enum with lock activities."""
23
+
24
+ LOCKING = "DOWN"
25
+ UNLOCKING = "UP"
26
+
27
+
28
+ class _LockError(StrEnum):
29
+ """Enum with lock errors."""
30
+
31
+ NO_ERROR = "NO_ERROR"
32
+ CLUTCH_FAILURE = "CLUTCH_FAILURE"
33
+ MOTOR_ABORTED = "MOTOR_ABORTED"
34
+
35
+
36
+ class _LockTargetLevel(StrEnum):
37
+ """Enum with lock target levels."""
38
+
39
+ LOCKED = "LOCKED"
40
+ OPEN = "OPEN"
41
+ UNLOCKED = "UNLOCKED"
42
+
43
+
44
+ class LockState(StrEnum):
45
+ """Enum with lock states."""
46
+
47
+ LOCKED = "LOCKED"
48
+ UNKNOWN = "UNKNOWN"
49
+ UNLOCKED = "UNLOCKED"
50
+
51
+
52
+ class BaseCustomDpLock(CustomDataPoint):
53
+ """Class for HomematicIP lock data point."""
54
+
55
+ __slots__ = ()
56
+
57
+ _category = DataPointCategory.LOCK
58
+ _ignore_multiple_channels_for_name = True
59
+
60
+ @state_property
61
+ @abstractmethod
62
+ def is_locked(self) -> bool:
63
+ """Return true if lock is on."""
64
+
65
+ @state_property
66
+ def is_jammed(self) -> bool:
67
+ """Return true if lock is jammed."""
68
+ return False
69
+
70
+ @state_property
71
+ def is_locking(self) -> bool | None:
72
+ """Return true if the lock is locking."""
73
+ return None
74
+
75
+ @state_property
76
+ def is_unlocking(self) -> bool | None:
77
+ """Return true if the lock is unlocking."""
78
+ return None
79
+
80
+ @property
81
+ @abstractmethod
82
+ def supports_open(self) -> bool:
83
+ """Flag if lock supports open."""
84
+
85
+ @abstractmethod
86
+ @bind_collector()
87
+ async def lock(self, *, collector: CallParameterCollector | None = None) -> None:
88
+ """Lock the lock."""
89
+
90
+ @abstractmethod
91
+ @bind_collector()
92
+ async def unlock(self, *, collector: CallParameterCollector | None = None) -> None:
93
+ """Unlock the lock."""
94
+
95
+ @abstractmethod
96
+ @bind_collector()
97
+ async def open(self, *, collector: CallParameterCollector | None = None) -> None:
98
+ """Open the lock."""
99
+
100
+
101
+ class CustomDpIpLock(BaseCustomDpLock):
102
+ """Class for HomematicIP lock data point."""
103
+
104
+ __slots__ = (
105
+ "_dp_direction",
106
+ "_dp_lock_state",
107
+ "_dp_lock_target_level",
108
+ )
109
+
110
+ def _init_data_point_fields(self) -> None:
111
+ """Init the data_point fields."""
112
+ super()._init_data_point_fields()
113
+ self._dp_lock_state: DpSensor[str | None] = self._get_data_point(
114
+ field=Field.LOCK_STATE, data_point_type=DpSensor[str | None]
115
+ )
116
+ self._dp_lock_target_level: DpAction = self._get_data_point(
117
+ field=Field.LOCK_TARGET_LEVEL, data_point_type=DpAction
118
+ )
119
+ self._dp_direction: DpSensor[str | None] = self._get_data_point(
120
+ field=Field.DIRECTION, data_point_type=DpSensor[str | None]
121
+ )
122
+
123
+ @state_property
124
+ def is_locked(self) -> bool:
125
+ """Return true if lock is on."""
126
+ return self._dp_lock_state.value == LockState.LOCKED
127
+
128
+ @state_property
129
+ def is_locking(self) -> bool | None:
130
+ """Return true if the lock is locking."""
131
+ if self._dp_direction.value is not None:
132
+ return str(self._dp_direction.value) == _LockActivity.LOCKING
133
+ return None
134
+
135
+ @state_property
136
+ def is_unlocking(self) -> bool | None:
137
+ """Return true if the lock is unlocking."""
138
+ if self._dp_direction.value is not None:
139
+ return str(self._dp_direction.value) == _LockActivity.UNLOCKING
140
+ return None
141
+
142
+ @property
143
+ def supports_open(self) -> bool:
144
+ """Flag if lock supports open."""
145
+ return True
146
+
147
+ @bind_collector()
148
+ async def lock(self, *, collector: CallParameterCollector | None = None) -> None:
149
+ """Lock the lock."""
150
+ await self._dp_lock_target_level.send_value(value=_LockTargetLevel.LOCKED, collector=collector)
151
+
152
+ @bind_collector()
153
+ async def unlock(self, *, collector: CallParameterCollector | None = None) -> None:
154
+ """Unlock the lock."""
155
+ await self._dp_lock_target_level.send_value(value=_LockTargetLevel.UNLOCKED, collector=collector)
156
+
157
+ @bind_collector()
158
+ async def open(self, *, collector: CallParameterCollector | None = None) -> None:
159
+ """Open the lock."""
160
+ await self._dp_lock_target_level.send_value(value=_LockTargetLevel.OPEN, collector=collector)
161
+
162
+
163
+ class CustomDpButtonLock(BaseCustomDpLock):
164
+ """Class for HomematicIP button lock data point."""
165
+
166
+ __slots__ = ("_dp_button_lock",)
167
+
168
+ def _init_data_point_fields(self) -> None:
169
+ """Init the data_point fields."""
170
+ super()._init_data_point_fields()
171
+ self._dp_button_lock: DpSwitch = self._get_data_point(field=Field.BUTTON_LOCK, data_point_type=DpSwitch)
172
+
173
+ @property
174
+ def data_point_name_postfix(self) -> str:
175
+ """Return the data_point name postfix."""
176
+ return "BUTTON_LOCK"
177
+
178
+ @state_property
179
+ def is_locked(self) -> bool:
180
+ """Return true if lock is on."""
181
+ return self._dp_button_lock.value is True
182
+
183
+ @property
184
+ def supports_open(self) -> bool:
185
+ """Flag if lock supports open."""
186
+ return False
187
+
188
+ @bind_collector()
189
+ async def lock(self, *, collector: CallParameterCollector | None = None) -> None:
190
+ """Lock the lock."""
191
+ await self._dp_button_lock.turn_on(collector=collector)
192
+
193
+ @bind_collector()
194
+ async def unlock(self, *, collector: CallParameterCollector | None = None) -> None:
195
+ """Unlock the lock."""
196
+ await self._dp_button_lock.turn_off(collector=collector)
197
+
198
+ @bind_collector()
199
+ async def open(self, *, collector: CallParameterCollector | None = None) -> None:
200
+ """Open the lock."""
201
+ return
202
+
203
+
204
+ class CustomDpRfLock(BaseCustomDpLock):
205
+ """Class for classic Homematic lock data point."""
206
+
207
+ __slots__ = (
208
+ "_dp_direction",
209
+ "_dp_error",
210
+ "_dp_open",
211
+ "_dp_state",
212
+ )
213
+
214
+ def _init_data_point_fields(self) -> None:
215
+ """Init the data_point fields."""
216
+ super()._init_data_point_fields()
217
+ self._dp_state: DpSwitch = self._get_data_point(field=Field.STATE, data_point_type=DpSwitch)
218
+ self._dp_open: DpAction = self._get_data_point(field=Field.OPEN, data_point_type=DpAction)
219
+ self._dp_direction: DpSensor[str | None] = self._get_data_point(
220
+ field=Field.DIRECTION, data_point_type=DpSensor[str | None]
221
+ )
222
+ self._dp_error: DpSensor[str | None] = self._get_data_point(
223
+ field=Field.ERROR, data_point_type=DpSensor[str | None]
224
+ )
225
+
226
+ @state_property
227
+ def is_locked(self) -> bool:
228
+ """Return true if lock is on."""
229
+ return self._dp_state.value is not True
230
+
231
+ @state_property
232
+ def is_locking(self) -> bool | None:
233
+ """Return true if the lock is locking."""
234
+ if self._dp_direction.value is not None:
235
+ return str(self._dp_direction.value) == _LockActivity.LOCKING
236
+ return None
237
+
238
+ @state_property
239
+ def is_unlocking(self) -> bool | None:
240
+ """Return true if the lock is unlocking."""
241
+ if self._dp_direction.value is not None:
242
+ return str(self._dp_direction.value) == _LockActivity.UNLOCKING
243
+ return None
244
+
245
+ @state_property
246
+ def is_jammed(self) -> bool:
247
+ """Return true if lock is jammed."""
248
+ return self._dp_error.value is not None and self._dp_error.value != _LockError.NO_ERROR
249
+
250
+ @property
251
+ def supports_open(self) -> bool:
252
+ """Flag if lock supports open."""
253
+ return True
254
+
255
+ @bind_collector()
256
+ async def lock(self, *, collector: CallParameterCollector | None = None) -> None:
257
+ """Lock the lock."""
258
+ await self._dp_state.send_value(value=False, collector=collector)
259
+
260
+ @bind_collector()
261
+ async def unlock(self, *, collector: CallParameterCollector | None = None) -> None:
262
+ """Unlock the lock."""
263
+ await self._dp_state.send_value(value=True, collector=collector)
264
+
265
+ @bind_collector()
266
+ async def open(self, *, collector: CallParameterCollector | None = None) -> None:
267
+ """Open the lock."""
268
+ await self._dp_open.send_value(value=True, collector=collector)
269
+
270
+
271
+ def make_ip_lock(
272
+ *,
273
+ channel: hmd.Channel,
274
+ custom_config: CustomConfig,
275
+ ) -> None:
276
+ """Create HomematicIP lock data point."""
277
+ hmed.make_custom_data_point(
278
+ channel=channel,
279
+ data_point_class=CustomDpIpLock,
280
+ device_profile=DeviceProfile.IP_LOCK,
281
+ custom_config=custom_config,
282
+ )
283
+
284
+
285
+ def make_ip_button_lock(
286
+ *,
287
+ channel: hmd.Channel,
288
+ custom_config: CustomConfig,
289
+ ) -> None:
290
+ """Create HomematicIP ip button lock data point."""
291
+ hmed.make_custom_data_point(
292
+ channel=channel,
293
+ data_point_class=CustomDpButtonLock,
294
+ device_profile=DeviceProfile.IP_BUTTON_LOCK,
295
+ custom_config=custom_config,
296
+ )
297
+
298
+
299
+ def make_rf_button_lock(
300
+ *,
301
+ channel: hmd.Channel,
302
+ custom_config: CustomConfig,
303
+ ) -> None:
304
+ """Create Homematic button lock data point."""
305
+ hmed.make_custom_data_point(
306
+ channel=channel,
307
+ data_point_class=CustomDpButtonLock,
308
+ device_profile=DeviceProfile.RF_BUTTON_LOCK,
309
+ custom_config=custom_config,
310
+ )
311
+
312
+
313
+ def make_rf_lock(
314
+ *,
315
+ channel: hmd.Channel,
316
+ custom_config: CustomConfig,
317
+ ) -> None:
318
+ """Create Homematic lock data point."""
319
+ hmed.make_custom_data_point(
320
+ channel=channel,
321
+ data_point_class=CustomDpRfLock,
322
+ device_profile=DeviceProfile.RF_LOCK,
323
+ custom_config=custom_config,
324
+ )
325
+
326
+
327
+ # Case for device model is not relevant.
328
+ # HomeBrew (HB-) devices are always listed as HM-.
329
+ DEVICES: Mapping[str, CustomConfig | tuple[CustomConfig, ...]] = {
330
+ "HM-Sec-Key": CustomConfig(
331
+ make_ce_func=make_rf_lock,
332
+ channels=(1,),
333
+ extended=ExtendedConfig(
334
+ additional_data_points={
335
+ 1: (
336
+ Parameter.DIRECTION,
337
+ Parameter.ERROR,
338
+ ),
339
+ }
340
+ ),
341
+ ),
342
+ "HmIP-DLD": (
343
+ CustomConfig(
344
+ make_ce_func=make_ip_lock,
345
+ extended=ExtendedConfig(
346
+ additional_data_points={
347
+ 0: (Parameter.ERROR_JAMMED,),
348
+ }
349
+ ),
350
+ ),
351
+ CustomConfig(
352
+ make_ce_func=make_ip_button_lock,
353
+ channels=(0,),
354
+ ),
355
+ ),
356
+ "HM-TC-IT-WM-W-EU": CustomConfig(
357
+ make_ce_func=make_rf_button_lock,
358
+ channels=(None,),
359
+ ),
360
+ "ALPHA-IP-RBG": CustomConfig(
361
+ make_ce_func=make_ip_button_lock,
362
+ channels=(0,),
363
+ ),
364
+ "HmIP-BWTH": CustomConfig(
365
+ make_ce_func=make_ip_button_lock,
366
+ channels=(0,),
367
+ ),
368
+ "HmIP-FAL": CustomConfig(
369
+ make_ce_func=make_ip_button_lock,
370
+ channels=(0,),
371
+ ),
372
+ "HmIP-WGT": CustomConfig(
373
+ make_ce_func=make_ip_button_lock,
374
+ channels=(0,),
375
+ ),
376
+ "HmIP-WTH": CustomConfig(
377
+ make_ce_func=make_ip_button_lock,
378
+ channels=(0,),
379
+ ),
380
+ "HmIP-eTRV": CustomConfig(
381
+ make_ce_func=make_ip_button_lock,
382
+ channels=(0,),
383
+ ),
384
+ "HmIPW-FAL": CustomConfig(
385
+ make_ce_func=make_ip_button_lock,
386
+ channels=(0,),
387
+ ),
388
+ "HmIPW-WTH": CustomConfig(
389
+ make_ce_func=make_ip_button_lock,
390
+ channels=(0,),
391
+ ),
392
+ }
393
+
394
+ hmed.ALL_DEVICES[DataPointCategory.LOCK] = DEVICES
@@ -0,0 +1,275 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025
3
+ """Module for data points implemented using the siren category."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from abc import abstractmethod
8
+ from collections.abc import Mapping
9
+ from enum import StrEnum
10
+ from typing import Final, TypedDict, Unpack
11
+
12
+ from aiohomematic.const import DataPointCategory, DeviceProfile, Field
13
+ from aiohomematic.exceptions import ValidationException
14
+ from aiohomematic.model import device as hmd
15
+ from aiohomematic.model.custom import definition as hmed
16
+ from aiohomematic.model.custom.data_point import CustomDataPoint
17
+ from aiohomematic.model.custom.support import CustomConfig
18
+ from aiohomematic.model.data_point import CallParameterCollector, bind_collector
19
+ from aiohomematic.model.generic import DpAction, DpBinarySensor, DpSensor
20
+ from aiohomematic.property_decorators import state_property
21
+
22
+ _SMOKE_DETECTOR_ALARM_STATUS_IDLE_OFF: Final = "IDLE_OFF"
23
+
24
+
25
+ class _SirenCommand(StrEnum):
26
+ """Enum with siren commands."""
27
+
28
+ OFF = "INTRUSION_ALARM_OFF"
29
+ ON = "INTRUSION_ALARM"
30
+
31
+
32
+ class SirenOnArgs(TypedDict, total=False):
33
+ """Matcher for the siren arguments."""
34
+
35
+ acoustic_alarm: str
36
+ optical_alarm: str
37
+ duration: str
38
+
39
+
40
+ class BaseCustomDpSiren(CustomDataPoint):
41
+ """Class for Homematic siren data point."""
42
+
43
+ __slots__ = ()
44
+
45
+ _category = DataPointCategory.SIREN
46
+
47
+ @state_property
48
+ @abstractmethod
49
+ def is_on(self) -> bool:
50
+ """Return true if siren is on."""
51
+
52
+ @state_property
53
+ @abstractmethod
54
+ def available_tones(self) -> tuple[str, ...] | None:
55
+ """Return available tones."""
56
+
57
+ @state_property
58
+ @abstractmethod
59
+ def available_lights(self) -> tuple[str, ...] | None:
60
+ """Return available lights."""
61
+
62
+ @property
63
+ @abstractmethod
64
+ def supports_duration(self) -> bool:
65
+ """Flag if siren supports duration."""
66
+
67
+ @property
68
+ def supports_tones(self) -> bool:
69
+ """Flag if siren supports tones."""
70
+ return self.available_tones is not None
71
+
72
+ @property
73
+ def supports_lights(self) -> bool:
74
+ """Flag if siren supports lights."""
75
+ return self.available_lights is not None
76
+
77
+ @abstractmethod
78
+ @bind_collector()
79
+ async def turn_on(
80
+ self,
81
+ *,
82
+ collector: CallParameterCollector | None = None,
83
+ **kwargs: Unpack[SirenOnArgs],
84
+ ) -> None:
85
+ """Turn the device on."""
86
+
87
+ @abstractmethod
88
+ @bind_collector()
89
+ async def turn_off(self, *, collector: CallParameterCollector | None = None) -> None:
90
+ """Turn the device off."""
91
+
92
+
93
+ class CustomDpIpSiren(BaseCustomDpSiren):
94
+ """Class for HomematicIP siren data point."""
95
+
96
+ __slots__ = (
97
+ "_dp_acoustic_alarm_active",
98
+ "_dp_acoustic_alarm_selection",
99
+ "_dp_duration",
100
+ "_dp_duration_unit",
101
+ "_dp_optical_alarm_active",
102
+ "_dp_optical_alarm_selection",
103
+ )
104
+
105
+ def _init_data_point_fields(self) -> None:
106
+ """Init the data_point fields."""
107
+ super()._init_data_point_fields()
108
+ self._dp_acoustic_alarm_active: DpBinarySensor = self._get_data_point(
109
+ field=Field.ACOUSTIC_ALARM_ACTIVE, data_point_type=DpBinarySensor
110
+ )
111
+ self._dp_acoustic_alarm_selection: DpAction = self._get_data_point(
112
+ field=Field.ACOUSTIC_ALARM_SELECTION, data_point_type=DpAction
113
+ )
114
+ self._dp_optical_alarm_active: DpBinarySensor = self._get_data_point(
115
+ field=Field.OPTICAL_ALARM_ACTIVE, data_point_type=DpBinarySensor
116
+ )
117
+ self._dp_optical_alarm_selection: DpAction = self._get_data_point(
118
+ field=Field.OPTICAL_ALARM_SELECTION, data_point_type=DpAction
119
+ )
120
+ self._dp_duration: DpAction = self._get_data_point(field=Field.DURATION, data_point_type=DpAction)
121
+ self._dp_duration_unit: DpAction = self._get_data_point(field=Field.DURATION_UNIT, data_point_type=DpAction)
122
+
123
+ @state_property
124
+ def is_on(self) -> bool:
125
+ """Return true if siren is on."""
126
+ return self._dp_acoustic_alarm_active.value is True or self._dp_optical_alarm_active.value is True
127
+
128
+ @state_property
129
+ def available_tones(self) -> tuple[str, ...] | None:
130
+ """Return available tones."""
131
+ return self._dp_acoustic_alarm_selection.values
132
+
133
+ @state_property
134
+ def available_lights(self) -> tuple[str, ...] | None:
135
+ """Return available lights."""
136
+ return self._dp_optical_alarm_selection.values
137
+
138
+ @property
139
+ def supports_duration(self) -> bool:
140
+ """Flag if siren supports duration."""
141
+ return True
142
+
143
+ @bind_collector()
144
+ async def turn_on(
145
+ self,
146
+ *,
147
+ collector: CallParameterCollector | None = None,
148
+ **kwargs: Unpack[SirenOnArgs],
149
+ ) -> None:
150
+ """Turn the device on."""
151
+
152
+ acoustic_alarm = kwargs.get("acoustic_alarm", self._dp_acoustic_alarm_selection.default)
153
+ if self.available_tones and acoustic_alarm and acoustic_alarm not in self.available_tones:
154
+ raise ValidationException(
155
+ f"Invalid tone specified for data_point {self.full_name}: {acoustic_alarm}, "
156
+ "check the available_tones attribute for valid tones to pass in"
157
+ )
158
+
159
+ optical_alarm = kwargs.get("optical_alarm", self._dp_optical_alarm_selection.default)
160
+ if self.available_lights and optical_alarm and optical_alarm not in self.available_lights:
161
+ raise ValidationException(
162
+ f"Invalid light specified for data_point {self.full_name}: {optical_alarm}, "
163
+ "check the available_lights attribute for valid tones to pass in"
164
+ )
165
+
166
+ await self._dp_acoustic_alarm_selection.send_value(value=acoustic_alarm, collector=collector)
167
+ await self._dp_optical_alarm_selection.send_value(value=optical_alarm, collector=collector)
168
+ await self._dp_duration_unit.send_value(value=self._dp_duration_unit.default, collector=collector)
169
+ duration = kwargs.get("duration", self._dp_duration.default)
170
+ await self._dp_duration.send_value(value=duration, collector=collector)
171
+
172
+ @bind_collector()
173
+ async def turn_off(self, *, collector: CallParameterCollector | None = None) -> None:
174
+ """Turn the device off."""
175
+ await self._dp_acoustic_alarm_selection.send_value(
176
+ value=self._dp_acoustic_alarm_selection.default, collector=collector
177
+ )
178
+ await self._dp_optical_alarm_selection.send_value(
179
+ value=self._dp_optical_alarm_selection.default, collector=collector
180
+ )
181
+ await self._dp_duration_unit.send_value(value=self._dp_duration_unit.default, collector=collector)
182
+ await self._dp_duration.send_value(value=self._dp_duration.default, collector=collector)
183
+
184
+
185
+ class CustomDpIpSirenSmoke(BaseCustomDpSiren):
186
+ """Class for HomematicIP siren smoke data point."""
187
+
188
+ __slots__ = (
189
+ "_dp_smoke_detector_alarm_status",
190
+ "_dp_smoke_detector_command",
191
+ )
192
+
193
+ def _init_data_point_fields(self) -> None:
194
+ """Init the data_point fields."""
195
+ super()._init_data_point_fields()
196
+ self._dp_smoke_detector_alarm_status: DpSensor[str | None] = self._get_data_point(
197
+ field=Field.SMOKE_DETECTOR_ALARM_STATUS, data_point_type=DpSensor[str | None]
198
+ )
199
+ self._dp_smoke_detector_command: DpAction = self._get_data_point(
200
+ field=Field.SMOKE_DETECTOR_COMMAND, data_point_type=DpAction
201
+ )
202
+
203
+ @state_property
204
+ def is_on(self) -> bool:
205
+ """Return true if siren is on."""
206
+ if not self._dp_smoke_detector_alarm_status.value:
207
+ return False
208
+ return bool(self._dp_smoke_detector_alarm_status.value != _SMOKE_DETECTOR_ALARM_STATUS_IDLE_OFF)
209
+
210
+ @state_property
211
+ def available_tones(self) -> tuple[str, ...] | None:
212
+ """Return available tones."""
213
+ return None
214
+
215
+ @state_property
216
+ def available_lights(self) -> tuple[str, ...] | None:
217
+ """Return available lights."""
218
+ return None
219
+
220
+ @property
221
+ def supports_duration(self) -> bool:
222
+ """Flag if siren supports duration."""
223
+ return False
224
+
225
+ @bind_collector()
226
+ async def turn_on(
227
+ self,
228
+ *,
229
+ collector: CallParameterCollector | None = None,
230
+ **kwargs: Unpack[SirenOnArgs],
231
+ ) -> None:
232
+ """Turn the device on."""
233
+ await self._dp_smoke_detector_command.send_value(value=_SirenCommand.ON, collector=collector)
234
+
235
+ @bind_collector()
236
+ async def turn_off(self, *, collector: CallParameterCollector | None = None) -> None:
237
+ """Turn the device off."""
238
+ await self._dp_smoke_detector_command.send_value(value=_SirenCommand.OFF, collector=collector)
239
+
240
+
241
+ def make_ip_siren(
242
+ *,
243
+ channel: hmd.Channel,
244
+ custom_config: CustomConfig,
245
+ ) -> None:
246
+ """Create HomematicIP siren data point."""
247
+ hmed.make_custom_data_point(
248
+ channel=channel,
249
+ data_point_class=CustomDpIpSiren,
250
+ device_profile=DeviceProfile.IP_SIREN,
251
+ custom_config=custom_config,
252
+ )
253
+
254
+
255
+ def make_ip_siren_smoke(
256
+ *,
257
+ channel: hmd.Channel,
258
+ custom_config: CustomConfig,
259
+ ) -> None:
260
+ """Create HomematicIP siren data point."""
261
+ hmed.make_custom_data_point(
262
+ channel=channel,
263
+ data_point_class=CustomDpIpSirenSmoke,
264
+ device_profile=DeviceProfile.IP_SIREN_SMOKE,
265
+ custom_config=custom_config,
266
+ )
267
+
268
+
269
+ # Case for device model is not relevant.
270
+ # HomeBrew (HB-) devices are always listed as HM-.
271
+ DEVICES: Mapping[str, CustomConfig | tuple[CustomConfig, ...]] = {
272
+ "HmIP-ASIR": CustomConfig(make_ce_func=make_ip_siren, channels=(3,)),
273
+ "HmIP-SWSD": CustomConfig(make_ce_func=make_ip_siren_smoke),
274
+ }
275
+ hmed.ALL_DEVICES[DataPointCategory.SIREN] = DEVICES
@@ -0,0 +1,41 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025
3
+ """Support classes used by aiohomematic custom data points."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Callable, Mapping
8
+ from dataclasses import dataclass
9
+
10
+ from aiohomematic.const import Field, Parameter
11
+
12
+
13
+ @dataclass(frozen=True, kw_only=True, slots=True)
14
+ class CustomConfig:
15
+ """Data for custom data_point creation."""
16
+
17
+ make_ce_func: Callable
18
+ channels: tuple[int | None, ...] = (1,)
19
+ extended: ExtendedConfig | None = None
20
+
21
+
22
+ @dataclass(frozen=True, kw_only=True, slots=True)
23
+ class ExtendedConfig:
24
+ """Extended data for custom data_point creation."""
25
+
26
+ fixed_channels: Mapping[int, Mapping[Field, Parameter]] | None = None
27
+ additional_data_points: Mapping[int | tuple[int, ...], tuple[Parameter, ...]] | None = None
28
+
29
+ @property
30
+ def required_parameters(self) -> tuple[Parameter, ...]:
31
+ """Return vol.Required parameters from extended config."""
32
+ required_parameters: list[Parameter] = []
33
+ if fixed_channels := self.fixed_channels:
34
+ for mapping in fixed_channels.values():
35
+ required_parameters.extend(mapping.values())
36
+
37
+ if additional_dps := self.additional_data_points:
38
+ for parameters in additional_dps.values():
39
+ required_parameters.extend(parameters)
40
+
41
+ return tuple(required_parameters)