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