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.
- aiohomematic/__init__.py +61 -0
- aiohomematic/async_support.py +212 -0
- aiohomematic/central/__init__.py +2309 -0
- aiohomematic/central/decorators.py +155 -0
- aiohomematic/central/rpc_server.py +295 -0
- aiohomematic/client/__init__.py +1848 -0
- aiohomematic/client/_rpc_errors.py +81 -0
- aiohomematic/client/json_rpc.py +1326 -0
- aiohomematic/client/rpc_proxy.py +311 -0
- aiohomematic/const.py +1127 -0
- aiohomematic/context.py +18 -0
- aiohomematic/converter.py +108 -0
- aiohomematic/decorators.py +302 -0
- aiohomematic/exceptions.py +164 -0
- aiohomematic/hmcli.py +186 -0
- aiohomematic/model/__init__.py +140 -0
- aiohomematic/model/calculated/__init__.py +84 -0
- aiohomematic/model/calculated/climate.py +290 -0
- aiohomematic/model/calculated/data_point.py +327 -0
- aiohomematic/model/calculated/operating_voltage_level.py +299 -0
- aiohomematic/model/calculated/support.py +234 -0
- aiohomematic/model/custom/__init__.py +177 -0
- aiohomematic/model/custom/climate.py +1532 -0
- aiohomematic/model/custom/cover.py +792 -0
- aiohomematic/model/custom/data_point.py +334 -0
- aiohomematic/model/custom/definition.py +871 -0
- aiohomematic/model/custom/light.py +1128 -0
- aiohomematic/model/custom/lock.py +394 -0
- aiohomematic/model/custom/siren.py +275 -0
- aiohomematic/model/custom/support.py +41 -0
- aiohomematic/model/custom/switch.py +175 -0
- aiohomematic/model/custom/valve.py +114 -0
- aiohomematic/model/data_point.py +1123 -0
- aiohomematic/model/device.py +1445 -0
- aiohomematic/model/event.py +208 -0
- aiohomematic/model/generic/__init__.py +217 -0
- aiohomematic/model/generic/action.py +34 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +27 -0
- aiohomematic/model/generic/data_point.py +171 -0
- aiohomematic/model/generic/dummy.py +147 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +39 -0
- aiohomematic/model/generic/sensor.py +74 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +29 -0
- aiohomematic/model/hub/__init__.py +333 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/data_point.py +340 -0
- aiohomematic/model/hub/number.py +39 -0
- aiohomematic/model/hub/select.py +49 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +44 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/support.py +586 -0
- aiohomematic/model/update.py +143 -0
- aiohomematic/property_decorators.py +496 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +92 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
- aiohomematic/rega_scripts/set_program_state.fn +12 -0
- aiohomematic/rega_scripts/set_system_variable.fn +15 -0
- aiohomematic/store/__init__.py +34 -0
- aiohomematic/store/dynamic.py +551 -0
- aiohomematic/store/persistent.py +988 -0
- aiohomematic/store/visibility.py +812 -0
- aiohomematic/support.py +664 -0
- aiohomematic/validator.py +112 -0
- aiohomematic-2025.11.3.dist-info/METADATA +144 -0
- aiohomematic-2025.11.3.dist-info/RECORD +77 -0
- aiohomematic-2025.11.3.dist-info/WHEEL +5 -0
- aiohomematic-2025.11.3.dist-info/entry_points.txt +2 -0
- aiohomematic-2025.11.3.dist-info/licenses/LICENSE +21 -0
- 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)
|