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,741 @@
1
+ """Module for data points implemented using the cover category."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Mapping
7
+ from enum import IntEnum, StrEnum
8
+ import logging
9
+ from typing import Any, Final
10
+
11
+ from aiohomematic.const import DataPointCategory, DataPointUsage, Parameter
12
+ from aiohomematic.converter import convert_hm_level_to_cpv
13
+ from aiohomematic.model import device as hmd
14
+ from aiohomematic.model.custom import definition as hmed
15
+ from aiohomematic.model.custom.const import DeviceProfile, Field
16
+ from aiohomematic.model.custom.data_point import CustomDataPoint
17
+ from aiohomematic.model.custom.support import CustomConfig, ExtendedConfig
18
+ from aiohomematic.model.data_point import CallParameterCollector, bind_collector
19
+ from aiohomematic.model.decorators import state_property
20
+ from aiohomematic.model.generic import DpAction, DpFloat, DpSelect, DpSensor
21
+
22
+ _LOGGER: Final = logging.getLogger(__name__)
23
+
24
+ _CLOSED_LEVEL: Final = 0.0
25
+ _COVER_VENT_MAX_POSITION: Final = 50
26
+ _LEVEL_TO_POSITION_MULTIPLIER: Final = 100.0
27
+ _MAX_LEVEL_POSITION: Final = 100.0
28
+ _MIN_LEVEL_POSITION: Final = 0.0
29
+ _OPEN_LEVEL: Final = 1.0
30
+ _OPEN_TILT_LEVEL: Final = 1.0
31
+ _WD_CLOSED_LEVEL: Final = -0.005
32
+
33
+
34
+ class _CoverActivity(StrEnum):
35
+ """Enum with cover activities."""
36
+
37
+ CLOSING = "DOWN"
38
+ OPENING = "UP"
39
+
40
+
41
+ class _CoverPosition(IntEnum):
42
+ """Enum with cover positions."""
43
+
44
+ OPEN = 100
45
+ VENT = 10
46
+ CLOSED = 0
47
+
48
+
49
+ class _GarageDoorActivity(IntEnum):
50
+ """Enum with garage door commands."""
51
+
52
+ CLOSING = 5
53
+ OPENING = 2
54
+
55
+
56
+ class _GarageDoorCommand(StrEnum):
57
+ """Enum with garage door commands."""
58
+
59
+ CLOSE = "CLOSE"
60
+ NOP = "NOP"
61
+ OPEN = "OPEN"
62
+ PARTIAL_OPEN = "PARTIAL_OPEN"
63
+ STOP = "STOP"
64
+
65
+
66
+ class _GarageDoorState(StrEnum):
67
+ """Enum with garage door states."""
68
+
69
+ CLOSED = "CLOSED"
70
+ OPEN = "OPEN"
71
+ VENTILATION_POSITION = "VENTILATION_POSITION"
72
+ POSITION_UNKNOWN = "_POSITION_UNKNOWN"
73
+
74
+
75
+ class _StateChangeArg(StrEnum):
76
+ """Enum with cover state change arguments."""
77
+
78
+ CLOSE = "close"
79
+ OPEN = "open"
80
+ POSITION = "position"
81
+ TILT_CLOSE = "tilt_close"
82
+ TILT_OPEN = "tilt_open"
83
+ TILT_POSITION = "tilt_position"
84
+ VENT = "vent"
85
+
86
+
87
+ class CustomDpCover(CustomDataPoint):
88
+ """Class for HomeMatic cover data point."""
89
+
90
+ __slots__ = (
91
+ "_command_processing_lock",
92
+ "_dp_direction",
93
+ "_dp_group_level",
94
+ "_dp_level",
95
+ "_dp_stop",
96
+ )
97
+ _category = DataPointCategory.COVER
98
+ _closed_level: float = _CLOSED_LEVEL
99
+ _closed_position: int = int(_CLOSED_LEVEL * _LEVEL_TO_POSITION_MULTIPLIER)
100
+ _open_level: float = _OPEN_LEVEL
101
+
102
+ def _init_data_point_fields(self) -> None:
103
+ """Init the data point fields."""
104
+ super()._init_data_point_fields()
105
+ self._command_processing_lock = asyncio.Lock()
106
+ self._dp_direction: DpSensor[str | None] = self._get_data_point(
107
+ field=Field.DIRECTION, data_point_type=DpSensor[str | None]
108
+ )
109
+ self._dp_level: DpFloat = self._get_data_point(field=Field.LEVEL, data_point_type=DpFloat)
110
+ self._dp_stop: DpAction = self._get_data_point(field=Field.STOP, data_point_type=DpAction)
111
+ self._dp_group_level: DpSensor[float | None] = self._get_data_point(
112
+ field=Field.GROUP_LEVEL, data_point_type=DpSensor[float | None]
113
+ )
114
+
115
+ @property
116
+ def _group_level(self) -> float:
117
+ """Return the channel level of the cover."""
118
+ if self._dp_group_level.value is not None and self.usage == DataPointUsage.CDP_PRIMARY:
119
+ return float(self._dp_group_level.value)
120
+ return self._dp_level.value if self._dp_level.value is not None else self._closed_level
121
+
122
+ @state_property
123
+ def current_channel_position(self) -> int:
124
+ """Return current channel position of cover."""
125
+ return (
126
+ int(self._dp_level.value * _LEVEL_TO_POSITION_MULTIPLIER)
127
+ if self._dp_level.value is not None
128
+ else self._closed_position
129
+ )
130
+
131
+ @state_property
132
+ def current_position(self) -> int:
133
+ """Return current group position of cover."""
134
+ return int(self._group_level * _LEVEL_TO_POSITION_MULTIPLIER)
135
+
136
+ @bind_collector()
137
+ async def set_position(
138
+ self,
139
+ position: int | None = None,
140
+ tilt_position: int | None = None,
141
+ collector: CallParameterCollector | None = None,
142
+ ) -> None:
143
+ """Move the cover to a specific position."""
144
+ if not self.is_state_change(position=position):
145
+ return
146
+ level = (
147
+ min(_MAX_LEVEL_POSITION, max(_MIN_LEVEL_POSITION, position)) / _MAX_LEVEL_POSITION
148
+ if position is not None
149
+ else None
150
+ )
151
+ await self._set_level(level=level, collector=collector)
152
+
153
+ async def _set_level(
154
+ self,
155
+ level: float | None = None,
156
+ tilt_level: float | None = None,
157
+ collector: CallParameterCollector | None = None,
158
+ ) -> None:
159
+ """Move the cover to a specific position. Value range is 0.0 to 1.01."""
160
+ if level is None:
161
+ return
162
+ await self._dp_level.send_value(value=level, collector=collector)
163
+
164
+ @state_property
165
+ def is_closed(self) -> bool | None:
166
+ """Return if the cover is closed."""
167
+ return self._group_level == self._closed_level
168
+
169
+ @state_property
170
+ def is_opening(self) -> bool | None:
171
+ """Return if the cover is opening."""
172
+ if self._dp_direction.value is not None:
173
+ return str(self._dp_direction.value) == _CoverActivity.OPENING
174
+ return None
175
+
176
+ @state_property
177
+ def is_closing(self) -> bool | None:
178
+ """Return if the cover is closing."""
179
+ if self._dp_direction.value is not None:
180
+ return str(self._dp_direction.value) == _CoverActivity.CLOSING
181
+ return None
182
+
183
+ @bind_collector()
184
+ async def open(self, collector: CallParameterCollector | None = None) -> None:
185
+ """Open the cover."""
186
+ if not self.is_state_change(open=True):
187
+ return
188
+ await self._set_level(level=self._open_level, collector=collector)
189
+
190
+ @bind_collector()
191
+ async def close(self, collector: CallParameterCollector | None = None) -> None:
192
+ """Close the cover."""
193
+ if not self.is_state_change(close=True):
194
+ return
195
+ await self._set_level(level=self._closed_level, collector=collector)
196
+
197
+ @bind_collector(enabled=False)
198
+ async def stop(self, collector: CallParameterCollector | None = None) -> None:
199
+ """Stop the device if in motion."""
200
+ await self._dp_stop.send_value(value=True, collector=collector)
201
+
202
+ def is_state_change(self, **kwargs: Any) -> bool:
203
+ """Check if the state changes due to kwargs."""
204
+ if kwargs.get(_StateChangeArg.OPEN) is not None and self._group_level != self._open_level:
205
+ return True
206
+ if kwargs.get(_StateChangeArg.CLOSE) is not None and self._group_level != self._closed_level:
207
+ return True
208
+ if (position := kwargs.get(_StateChangeArg.POSITION)) is not None and position != self.current_position:
209
+ return True
210
+ return super().is_state_change(**kwargs)
211
+
212
+
213
+ class CustomDpWindowDrive(CustomDpCover):
214
+ """Class for Homematic window drive."""
215
+
216
+ __slots__ = ()
217
+
218
+ _closed_level: float = _WD_CLOSED_LEVEL
219
+ _open_level: float = _OPEN_LEVEL
220
+
221
+ @state_property
222
+ def current_position(self) -> int:
223
+ """Return current position of cover."""
224
+ level = self._dp_level.value if self._dp_level.value is not None else self._closed_level
225
+ if level == _WD_CLOSED_LEVEL:
226
+ level = _CLOSED_LEVEL
227
+ elif level == _CLOSED_LEVEL:
228
+ level = 0.01
229
+ return int(level * _LEVEL_TO_POSITION_MULTIPLIER)
230
+
231
+ async def _set_level(
232
+ self,
233
+ level: float | None = None,
234
+ tilt_level: float | None = None,
235
+ collector: CallParameterCollector | None = None,
236
+ ) -> None:
237
+ """Move the window drive to a specific position. Value range is -0.005 to 1.01."""
238
+ if level is None:
239
+ return
240
+
241
+ if level == _CLOSED_LEVEL:
242
+ wd_level = _WD_CLOSED_LEVEL
243
+ elif _CLOSED_LEVEL < level <= 0.01:
244
+ wd_level = 0
245
+ else:
246
+ wd_level = level
247
+ await self._dp_level.send_value(value=wd_level, collector=collector, do_validate=False)
248
+
249
+
250
+ class CustomDpBlind(CustomDpCover):
251
+ """Class for HomeMatic blind data point."""
252
+
253
+ __slots__ = (
254
+ "_dp_combined",
255
+ "_dp_group_level_2",
256
+ "_dp_level_2",
257
+ )
258
+ _open_tilt_level: float = _OPEN_TILT_LEVEL
259
+
260
+ def _init_data_point_fields(self) -> None:
261
+ """Init the data point fields."""
262
+ super()._init_data_point_fields()
263
+ self._dp_group_level_2: DpSensor[float | None] = self._get_data_point(
264
+ field=Field.GROUP_LEVEL_2, data_point_type=DpSensor[float | None]
265
+ )
266
+ self._dp_level_2: DpFloat = self._get_data_point(field=Field.LEVEL_2, data_point_type=DpFloat)
267
+ self._dp_combined: DpAction = self._get_data_point(field=Field.LEVEL_COMBINED, data_point_type=DpAction)
268
+
269
+ @property
270
+ def _group_tilt_level(self) -> float:
271
+ """Return the group level of the tilt."""
272
+ if self._dp_group_level_2.value is not None and self.usage == DataPointUsage.CDP_PRIMARY:
273
+ return float(self._dp_group_level_2.value)
274
+ return self._dp_level_2.value if self._dp_level_2.value is not None else self._closed_level
275
+
276
+ @state_property
277
+ def current_channel_tilt_position(self) -> int:
278
+ """Return current channel_tilt position of cover."""
279
+ return (
280
+ int(self._dp_level_2.value * _LEVEL_TO_POSITION_MULTIPLIER)
281
+ if self._dp_level_2.value is not None
282
+ else self._closed_position
283
+ )
284
+
285
+ @state_property
286
+ def current_tilt_position(self) -> int:
287
+ """Return current tilt position of cover."""
288
+ return int(self._group_tilt_level * _LEVEL_TO_POSITION_MULTIPLIER)
289
+
290
+ @property
291
+ def _target_level(self) -> float | None:
292
+ """Return the level of last service call."""
293
+ if (last_value_send := self._dp_level.unconfirmed_last_value_send) is not None:
294
+ return float(last_value_send)
295
+ return None
296
+
297
+ @property
298
+ def _target_tilt_level(self) -> float | None:
299
+ """Return the tilt level of last service call."""
300
+ if (last_value_send := self._dp_level_2.unconfirmed_last_value_send) is not None:
301
+ return float(last_value_send)
302
+ return None
303
+
304
+ @bind_collector(enabled=False)
305
+ async def set_position(
306
+ self,
307
+ position: int | None = None,
308
+ tilt_position: int | None = None,
309
+ collector: CallParameterCollector | None = None,
310
+ ) -> None:
311
+ """Move the blind to a specific position."""
312
+ if not self.is_state_change(position=position, tilt_position=tilt_position):
313
+ return
314
+ level = (
315
+ min(_MAX_LEVEL_POSITION, max(_MIN_LEVEL_POSITION, position)) / _MAX_LEVEL_POSITION
316
+ if position is not None
317
+ else None
318
+ )
319
+ tilt_level = (
320
+ min(_MAX_LEVEL_POSITION, max(_MIN_LEVEL_POSITION, tilt_position)) / _MAX_LEVEL_POSITION
321
+ if tilt_position is not None
322
+ else None
323
+ )
324
+ await self._set_level(level=level, tilt_level=tilt_level, collector=collector)
325
+
326
+ async def _set_level(
327
+ self,
328
+ level: float | None = None,
329
+ tilt_level: float | None = None,
330
+ collector: CallParameterCollector | None = None,
331
+ ) -> None:
332
+ """
333
+ Move the cover to a specific tilt level. Value range is 0.0 to 1.00.
334
+
335
+ level or tilt_level may be set to None for no change.
336
+ """
337
+ currently_moving = False
338
+
339
+ async with self._command_processing_lock:
340
+ if level is not None:
341
+ _level = level
342
+ elif self._target_level is not None:
343
+ # The blind moves and the target blind height is known
344
+ currently_moving = True
345
+ _level = self._target_level
346
+ else: # The blind is at a standstill and no level is explicitly requested => we remain at the current level
347
+ _level = self._group_level
348
+
349
+ if tilt_level is not None:
350
+ _tilt_level = tilt_level
351
+ elif self._target_tilt_level is not None:
352
+ # The blind moves and the target slat position is known
353
+ currently_moving = True
354
+ _tilt_level = self._target_tilt_level
355
+ else: # The blind is at a standstill and no tilt is explicitly desired => we remain at the current angle
356
+ _tilt_level = self._group_tilt_level
357
+
358
+ if currently_moving:
359
+ # Blind actors are buggy when sending new coordinates while they are moving. So we stop them first.
360
+ await self._stop()
361
+
362
+ await self._send_level(level=_level, tilt_level=_tilt_level, collector=collector)
363
+
364
+ @bind_collector()
365
+ async def _send_level(
366
+ self,
367
+ level: float,
368
+ tilt_level: float,
369
+ collector: CallParameterCollector | None = None,
370
+ ) -> None:
371
+ """Transmit a new target level to the device."""
372
+ if self._dp_combined.is_hmtype and (
373
+ combined_parameter := self._get_combined_value(level=level, tilt_level=tilt_level)
374
+ ):
375
+ # don't use collector for blind combined parameter
376
+ await self._dp_combined.send_value(value=combined_parameter, collector=None)
377
+ return
378
+
379
+ await self._dp_level_2.send_value(value=tilt_level, collector=collector)
380
+ await super()._set_level(level=level, collector=collector)
381
+
382
+ @bind_collector(enabled=False)
383
+ async def open(self, collector: CallParameterCollector | None = None) -> None:
384
+ """Open the cover and open the tilt."""
385
+ if not self.is_state_change(open=True, tilt_open=True):
386
+ return
387
+ await self._set_level(
388
+ level=self._open_level,
389
+ tilt_level=self._open_tilt_level,
390
+ collector=collector,
391
+ )
392
+
393
+ @bind_collector(enabled=False)
394
+ async def close(self, collector: CallParameterCollector | None = None) -> None:
395
+ """Close the cover and close the tilt."""
396
+ if not self.is_state_change(close=True, tilt_close=True):
397
+ return
398
+ await self._set_level(
399
+ level=self._closed_level,
400
+ tilt_level=self._closed_level,
401
+ collector=collector,
402
+ )
403
+
404
+ @bind_collector(enabled=False)
405
+ async def stop(self, collector: CallParameterCollector | None = None) -> None:
406
+ """Stop the device if in motion."""
407
+ async with self._command_processing_lock:
408
+ await self._stop(collector=collector)
409
+
410
+ @bind_collector(enabled=False)
411
+ async def _stop(self, collector: CallParameterCollector | None = None) -> None:
412
+ """Stop the device if in motion. Do only call with _command_processing_lock held."""
413
+ await super().stop(collector=collector)
414
+
415
+ @bind_collector(enabled=False)
416
+ async def open_tilt(self, collector: CallParameterCollector | None = None) -> None:
417
+ """Open the tilt."""
418
+ if not self.is_state_change(tilt_open=True):
419
+ return
420
+ await self._set_level(tilt_level=self._open_tilt_level, collector=collector)
421
+
422
+ @bind_collector(enabled=False)
423
+ async def close_tilt(self, collector: CallParameterCollector | None = None) -> None:
424
+ """Close the tilt."""
425
+ if not self.is_state_change(tilt_close=True):
426
+ return
427
+ await self._set_level(tilt_level=self._closed_level, collector=collector)
428
+
429
+ @bind_collector(enabled=False)
430
+ async def stop_tilt(self, collector: CallParameterCollector | None = None) -> None:
431
+ """Stop the device if in motion. Use only when command_processing_lock is held."""
432
+ await self.stop(collector=collector)
433
+
434
+ def is_state_change(self, **kwargs: Any) -> bool:
435
+ """Check if the state changes due to kwargs."""
436
+ if (
437
+ tilt_position := kwargs.get(_StateChangeArg.TILT_POSITION)
438
+ ) is not None and tilt_position != self.current_tilt_position:
439
+ return True
440
+ if kwargs.get(_StateChangeArg.TILT_OPEN) is not None and self.current_tilt_position != _CoverPosition.OPEN:
441
+ return True
442
+ if kwargs.get(_StateChangeArg.TILT_CLOSE) is not None and self.current_tilt_position != _CoverPosition.CLOSED:
443
+ return True
444
+ return super().is_state_change(**kwargs)
445
+
446
+ def _get_combined_value(self, level: float | None = None, tilt_level: float | None = None) -> str | None:
447
+ """Return the combined parameter."""
448
+ if level is None and tilt_level is None:
449
+ return None
450
+ levels: list[str] = []
451
+ # the resulting hex value is based on the doubled position
452
+ if level is not None:
453
+ levels.append(convert_hm_level_to_cpv(hm_level=level))
454
+ if tilt_level is not None:
455
+ levels.append(convert_hm_level_to_cpv(hm_level=tilt_level))
456
+
457
+ if levels:
458
+ return ",".join(levels)
459
+ return None
460
+
461
+
462
+ class CustomDpIpBlind(CustomDpBlind):
463
+ """Class for HomematicIP blind data point."""
464
+
465
+ __slots__ = ("_dp_operation_mode",)
466
+
467
+ def _init_data_point_fields(self) -> None:
468
+ """Init the data point fields."""
469
+ super()._init_data_point_fields()
470
+ self._dp_operation_mode: DpSelect = self._get_data_point(field=Field.OPERATION_MODE, data_point_type=DpSelect)
471
+ self._dp_combined: DpAction = self._get_data_point(field=Field.COMBINED_PARAMETER, data_point_type=DpAction)
472
+
473
+ @property
474
+ def operation_mode(self) -> str | None:
475
+ """Return operation mode of cover."""
476
+ return self._dp_operation_mode.value
477
+
478
+ def _get_combined_value(self, level: float | None = None, tilt_level: float | None = None) -> str | None:
479
+ """Return the combined parameter."""
480
+ if level is None and tilt_level is None:
481
+ return None
482
+ levels: list[str] = []
483
+ if tilt_level is not None:
484
+ levels.append(f"L2={int(tilt_level * _LEVEL_TO_POSITION_MULTIPLIER)}")
485
+ if level is not None:
486
+ levels.append(f"L={int(level * _LEVEL_TO_POSITION_MULTIPLIER)}")
487
+
488
+ if levels:
489
+ return ",".join(levels)
490
+ return None
491
+
492
+
493
+ class CustomDpGarage(CustomDataPoint):
494
+ """Class for HomeMatic garage data point."""
495
+
496
+ __slots__ = (
497
+ "_dp_door_command",
498
+ "_dp_door_state",
499
+ "_dp_section",
500
+ )
501
+ _category = DataPointCategory.COVER
502
+
503
+ def _init_data_point_fields(self) -> None:
504
+ """Init the data point fields."""
505
+ super()._init_data_point_fields()
506
+ self._dp_door_state: DpSensor[str | None] = self._get_data_point(
507
+ field=Field.DOOR_STATE, data_point_type=DpSensor[str | None]
508
+ )
509
+ self._dp_door_command: DpAction = self._get_data_point(field=Field.DOOR_COMMAND, data_point_type=DpAction)
510
+ self._dp_section: DpSensor[str | None] = self._get_data_point(
511
+ field=Field.SECTION, data_point_type=DpSensor[str | None]
512
+ )
513
+
514
+ @state_property
515
+ def current_position(self) -> int | None:
516
+ """Return current position of the garage door ."""
517
+ if self._dp_door_state.value == _GarageDoorState.OPEN:
518
+ return _CoverPosition.OPEN
519
+ if self._dp_door_state.value == _GarageDoorState.VENTILATION_POSITION:
520
+ return _CoverPosition.VENT
521
+ if self._dp_door_state.value == _GarageDoorState.CLOSED:
522
+ return _CoverPosition.CLOSED
523
+ return None
524
+
525
+ @bind_collector()
526
+ async def set_position(
527
+ self,
528
+ position: int | None = None,
529
+ tilt_position: int | None = None,
530
+ collector: CallParameterCollector | None = None,
531
+ ) -> None:
532
+ """Move the garage door to a specific position."""
533
+ if position is None:
534
+ return
535
+ if _COVER_VENT_MAX_POSITION < position <= _CoverPosition.OPEN:
536
+ await self.open(collector=collector)
537
+ if _CoverPosition.VENT < position <= _COVER_VENT_MAX_POSITION:
538
+ await self.vent(collector=collector)
539
+ if _CoverPosition.CLOSED <= position <= _CoverPosition.VENT:
540
+ await self.close(collector=collector)
541
+
542
+ @state_property
543
+ def is_closed(self) -> bool | None:
544
+ """Return if the garage door is closed."""
545
+ if self._dp_door_state.value is not None:
546
+ return str(self._dp_door_state.value) == _GarageDoorState.CLOSED
547
+ return None
548
+
549
+ @state_property
550
+ def is_opening(self) -> bool | None:
551
+ """Return if the garage door is opening."""
552
+ if self._dp_section.value is not None:
553
+ return int(self._dp_section.value) == _GarageDoorActivity.OPENING
554
+ return None
555
+
556
+ @state_property
557
+ def is_closing(self) -> bool | None:
558
+ """Return if the garage door is closing."""
559
+ if self._dp_section.value is not None:
560
+ return int(self._dp_section.value) == _GarageDoorActivity.CLOSING
561
+ return None
562
+
563
+ @bind_collector()
564
+ async def open(self, collector: CallParameterCollector | None = None) -> None:
565
+ """Open the garage door."""
566
+ if not self.is_state_change(open=True):
567
+ return
568
+ await self._dp_door_command.send_value(value=_GarageDoorCommand.OPEN, collector=collector)
569
+
570
+ @bind_collector()
571
+ async def close(self, collector: CallParameterCollector | None = None) -> None:
572
+ """Close the garage door."""
573
+ if not self.is_state_change(close=True):
574
+ return
575
+ await self._dp_door_command.send_value(value=_GarageDoorCommand.CLOSE, collector=collector)
576
+
577
+ @bind_collector(enabled=False)
578
+ async def stop(self, collector: CallParameterCollector | None = None) -> None:
579
+ """Stop the device if in motion."""
580
+ await self._dp_door_command.send_value(value=_GarageDoorCommand.STOP, collector=collector)
581
+
582
+ @bind_collector()
583
+ async def vent(self, collector: CallParameterCollector | None = None) -> None:
584
+ """Move the garage door to vent position."""
585
+ if not self.is_state_change(vent=True):
586
+ return
587
+ await self._dp_door_command.send_value(value=_GarageDoorCommand.PARTIAL_OPEN, collector=collector)
588
+
589
+ def is_state_change(self, **kwargs: Any) -> bool:
590
+ """Check if the state changes due to kwargs."""
591
+ if kwargs.get(_StateChangeArg.OPEN) is not None and self.current_position != _CoverPosition.OPEN:
592
+ return True
593
+ if kwargs.get(_StateChangeArg.VENT) is not None and self.current_position != _CoverPosition.VENT:
594
+ return True
595
+ if kwargs.get(_StateChangeArg.CLOSE) is not None and self.current_position != _CoverPosition.CLOSED:
596
+ return True
597
+ return super().is_state_change(**kwargs)
598
+
599
+
600
+ def make_ip_cover(
601
+ channel: hmd.Channel,
602
+ custom_config: CustomConfig,
603
+ ) -> None:
604
+ """Create HomematicIP cover data point."""
605
+ hmed.make_custom_data_point(
606
+ channel=channel,
607
+ data_point_class=CustomDpCover,
608
+ device_profile=DeviceProfile.IP_COVER,
609
+ custom_config=custom_config,
610
+ )
611
+
612
+
613
+ def make_rf_cover(
614
+ channel: hmd.Channel,
615
+ custom_config: CustomConfig,
616
+ ) -> None:
617
+ """Create HomeMatic classic cover data point."""
618
+ hmed.make_custom_data_point(
619
+ channel=channel,
620
+ data_point_class=CustomDpCover,
621
+ device_profile=DeviceProfile.RF_COVER,
622
+ custom_config=custom_config,
623
+ )
624
+
625
+
626
+ def make_ip_blind(
627
+ channel: hmd.Channel,
628
+ custom_config: CustomConfig,
629
+ ) -> None:
630
+ """Create HomematicIP cover data point."""
631
+ hmed.make_custom_data_point(
632
+ channel=channel,
633
+ data_point_class=CustomDpIpBlind,
634
+ device_profile=DeviceProfile.IP_COVER,
635
+ custom_config=custom_config,
636
+ )
637
+
638
+
639
+ def make_ip_garage(
640
+ channel: hmd.Channel,
641
+ custom_config: CustomConfig,
642
+ ) -> None:
643
+ """Create HomematicIP garage data point."""
644
+ hmed.make_custom_data_point(
645
+ channel=channel,
646
+ data_point_class=CustomDpGarage,
647
+ device_profile=DeviceProfile.IP_GARAGE,
648
+ custom_config=custom_config,
649
+ )
650
+
651
+
652
+ def make_ip_hdm(
653
+ channel: hmd.Channel,
654
+ custom_config: CustomConfig,
655
+ ) -> None:
656
+ """Create HomematicIP cover data point."""
657
+ hmed.make_custom_data_point(
658
+ channel=channel,
659
+ data_point_class=CustomDpIpBlind,
660
+ device_profile=DeviceProfile.IP_HDM,
661
+ custom_config=custom_config,
662
+ )
663
+
664
+
665
+ def make_rf_blind(
666
+ channel: hmd.Channel,
667
+ custom_config: CustomConfig,
668
+ ) -> None:
669
+ """Create HomeMatic classic cover data point."""
670
+ hmed.make_custom_data_point(
671
+ channel=channel,
672
+ data_point_class=CustomDpBlind,
673
+ device_profile=DeviceProfile.RF_COVER,
674
+ custom_config=custom_config,
675
+ )
676
+
677
+
678
+ def make_rf_window_drive(
679
+ channel: hmd.Channel,
680
+ custom_config: CustomConfig,
681
+ ) -> None:
682
+ """Create HomeMatic classic window drive data point."""
683
+ hmed.make_custom_data_point(
684
+ channel=channel,
685
+ data_point_class=CustomDpWindowDrive,
686
+ device_profile=DeviceProfile.RF_COVER,
687
+ custom_config=custom_config,
688
+ )
689
+
690
+
691
+ # Case for device model is not relevant.
692
+ # HomeBrew (HB-) devices are always listed as HM-.
693
+ DEVICES: Mapping[str, CustomConfig | tuple[CustomConfig, ...]] = {
694
+ "263 146": CustomConfig(make_ce_func=make_rf_cover),
695
+ "263 147": CustomConfig(make_ce_func=make_rf_cover),
696
+ "HM-LC-Bl1-Velux": CustomConfig(make_ce_func=make_rf_cover), # HB-LC-Bl1-Velux
697
+ "HM-LC-Bl1-FM": CustomConfig(make_ce_func=make_rf_cover),
698
+ "HM-LC-Bl1-FM-2": CustomConfig(make_ce_func=make_rf_cover),
699
+ "HM-LC-Bl1-PB-FM": CustomConfig(make_ce_func=make_rf_cover),
700
+ "HM-LC-Bl1-SM": CustomConfig(make_ce_func=make_rf_cover),
701
+ "HM-LC-Bl1-SM-2": CustomConfig(make_ce_func=make_rf_cover),
702
+ "HM-LC-Bl1PBU-FM": CustomConfig(make_ce_func=make_rf_cover),
703
+ "HM-LC-BlX": CustomConfig(make_ce_func=make_rf_cover),
704
+ "HM-LC-Ja1PBU-FM": CustomConfig(make_ce_func=make_rf_blind),
705
+ "HM-LC-JaX": CustomConfig(make_ce_func=make_rf_blind),
706
+ "HM-Sec-Win": CustomConfig(
707
+ make_ce_func=make_rf_window_drive,
708
+ channels=(1,),
709
+ extended=ExtendedConfig(
710
+ additional_data_points={
711
+ 1: (
712
+ Parameter.DIRECTION,
713
+ Parameter.WORKING,
714
+ Parameter.ERROR,
715
+ ),
716
+ 2: (
717
+ Parameter.LEVEL,
718
+ Parameter.STATUS,
719
+ ),
720
+ }
721
+ ),
722
+ ),
723
+ "HMW-LC-Bl1": CustomConfig(make_ce_func=make_rf_cover, channels=(3,)),
724
+ "HmIP-BBL": CustomConfig(make_ce_func=make_ip_blind, channels=(4,)),
725
+ "HmIP-BROLL": CustomConfig(make_ce_func=make_ip_cover, channels=(4,)),
726
+ "HmIP-DRBLI4": CustomConfig(
727
+ make_ce_func=make_ip_blind,
728
+ channels=(10, 14, 18, 22),
729
+ ),
730
+ "HmIP-FBL": CustomConfig(make_ce_func=make_ip_blind, channels=(4,)),
731
+ "HmIP-FROLL": CustomConfig(make_ce_func=make_ip_cover, channels=(4,)),
732
+ "HmIP-HDM": CustomConfig(make_ce_func=make_ip_hdm),
733
+ "HmIP-MOD-HO": CustomConfig(make_ce_func=make_ip_garage),
734
+ "HmIP-MOD-TM": CustomConfig(make_ce_func=make_ip_garage),
735
+ "HmIPW-DRBL4": CustomConfig(
736
+ make_ce_func=make_ip_blind,
737
+ channels=(2, 6, 10, 14),
738
+ ),
739
+ "ZEL STG RM FEP 230V": CustomConfig(make_ce_func=make_rf_cover),
740
+ }
741
+ hmed.ALL_DEVICES[DataPointCategory.COVER] = DEVICES