aiohomematic 2026.1.29__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.
- aiohomematic/__init__.py +110 -0
- aiohomematic/_log_context_protocol.py +29 -0
- aiohomematic/api.py +410 -0
- aiohomematic/async_support.py +250 -0
- aiohomematic/backend_detection.py +462 -0
- aiohomematic/central/__init__.py +103 -0
- aiohomematic/central/async_rpc_server.py +760 -0
- aiohomematic/central/central_unit.py +1152 -0
- aiohomematic/central/config.py +463 -0
- aiohomematic/central/config_builder.py +772 -0
- aiohomematic/central/connection_state.py +160 -0
- aiohomematic/central/coordinators/__init__.py +38 -0
- aiohomematic/central/coordinators/cache.py +414 -0
- aiohomematic/central/coordinators/client.py +480 -0
- aiohomematic/central/coordinators/connection_recovery.py +1141 -0
- aiohomematic/central/coordinators/device.py +1166 -0
- aiohomematic/central/coordinators/event.py +514 -0
- aiohomematic/central/coordinators/hub.py +532 -0
- aiohomematic/central/decorators.py +184 -0
- aiohomematic/central/device_registry.py +229 -0
- aiohomematic/central/events/__init__.py +104 -0
- aiohomematic/central/events/bus.py +1392 -0
- aiohomematic/central/events/integration.py +424 -0
- aiohomematic/central/events/types.py +194 -0
- aiohomematic/central/health.py +762 -0
- aiohomematic/central/rpc_server.py +353 -0
- aiohomematic/central/scheduler.py +794 -0
- aiohomematic/central/state_machine.py +391 -0
- aiohomematic/client/__init__.py +203 -0
- aiohomematic/client/_rpc_errors.py +187 -0
- aiohomematic/client/backends/__init__.py +48 -0
- aiohomematic/client/backends/base.py +335 -0
- aiohomematic/client/backends/capabilities.py +138 -0
- aiohomematic/client/backends/ccu.py +487 -0
- aiohomematic/client/backends/factory.py +116 -0
- aiohomematic/client/backends/homegear.py +294 -0
- aiohomematic/client/backends/json_ccu.py +252 -0
- aiohomematic/client/backends/protocol.py +316 -0
- aiohomematic/client/ccu.py +1857 -0
- aiohomematic/client/circuit_breaker.py +459 -0
- aiohomematic/client/config.py +64 -0
- aiohomematic/client/handlers/__init__.py +40 -0
- aiohomematic/client/handlers/backup.py +157 -0
- aiohomematic/client/handlers/base.py +79 -0
- aiohomematic/client/handlers/device_ops.py +1085 -0
- aiohomematic/client/handlers/firmware.py +144 -0
- aiohomematic/client/handlers/link_mgmt.py +199 -0
- aiohomematic/client/handlers/metadata.py +436 -0
- aiohomematic/client/handlers/programs.py +144 -0
- aiohomematic/client/handlers/sysvars.py +100 -0
- aiohomematic/client/interface_client.py +1304 -0
- aiohomematic/client/json_rpc.py +2068 -0
- aiohomematic/client/request_coalescer.py +282 -0
- aiohomematic/client/rpc_proxy.py +629 -0
- aiohomematic/client/state_machine.py +324 -0
- aiohomematic/const.py +2207 -0
- aiohomematic/context.py +275 -0
- aiohomematic/converter.py +270 -0
- aiohomematic/decorators.py +390 -0
- aiohomematic/exceptions.py +185 -0
- aiohomematic/hmcli.py +997 -0
- aiohomematic/i18n.py +193 -0
- aiohomematic/interfaces/__init__.py +407 -0
- aiohomematic/interfaces/central.py +1067 -0
- aiohomematic/interfaces/client.py +1096 -0
- aiohomematic/interfaces/coordinators.py +63 -0
- aiohomematic/interfaces/model.py +1921 -0
- aiohomematic/interfaces/operations.py +217 -0
- aiohomematic/logging_context.py +134 -0
- aiohomematic/metrics/__init__.py +125 -0
- aiohomematic/metrics/_protocols.py +140 -0
- aiohomematic/metrics/aggregator.py +534 -0
- aiohomematic/metrics/dataclasses.py +489 -0
- aiohomematic/metrics/emitter.py +292 -0
- aiohomematic/metrics/events.py +183 -0
- aiohomematic/metrics/keys.py +300 -0
- aiohomematic/metrics/observer.py +563 -0
- aiohomematic/metrics/stats.py +172 -0
- aiohomematic/model/__init__.py +189 -0
- aiohomematic/model/availability.py +65 -0
- aiohomematic/model/calculated/__init__.py +89 -0
- aiohomematic/model/calculated/climate.py +276 -0
- aiohomematic/model/calculated/data_point.py +315 -0
- aiohomematic/model/calculated/field.py +147 -0
- aiohomematic/model/calculated/operating_voltage_level.py +286 -0
- aiohomematic/model/calculated/support.py +232 -0
- aiohomematic/model/custom/__init__.py +214 -0
- aiohomematic/model/custom/capabilities/__init__.py +67 -0
- aiohomematic/model/custom/capabilities/climate.py +41 -0
- aiohomematic/model/custom/capabilities/light.py +87 -0
- aiohomematic/model/custom/capabilities/lock.py +44 -0
- aiohomematic/model/custom/capabilities/siren.py +63 -0
- aiohomematic/model/custom/climate.py +1130 -0
- aiohomematic/model/custom/cover.py +722 -0
- aiohomematic/model/custom/data_point.py +360 -0
- aiohomematic/model/custom/definition.py +300 -0
- aiohomematic/model/custom/field.py +89 -0
- aiohomematic/model/custom/light.py +1174 -0
- aiohomematic/model/custom/lock.py +322 -0
- aiohomematic/model/custom/mixins.py +445 -0
- aiohomematic/model/custom/profile.py +945 -0
- aiohomematic/model/custom/registry.py +251 -0
- aiohomematic/model/custom/siren.py +462 -0
- aiohomematic/model/custom/switch.py +195 -0
- aiohomematic/model/custom/text_display.py +289 -0
- aiohomematic/model/custom/valve.py +78 -0
- aiohomematic/model/data_point.py +1416 -0
- aiohomematic/model/device.py +1840 -0
- aiohomematic/model/event.py +216 -0
- aiohomematic/model/generic/__init__.py +327 -0
- aiohomematic/model/generic/action.py +40 -0
- aiohomematic/model/generic/action_select.py +62 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +31 -0
- aiohomematic/model/generic/data_point.py +177 -0
- aiohomematic/model/generic/dummy.py +150 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +56 -0
- aiohomematic/model/generic/sensor.py +76 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +33 -0
- aiohomematic/model/hub/__init__.py +100 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/connectivity.py +190 -0
- aiohomematic/model/hub/data_point.py +342 -0
- aiohomematic/model/hub/hub.py +864 -0
- aiohomematic/model/hub/inbox.py +135 -0
- aiohomematic/model/hub/install_mode.py +393 -0
- aiohomematic/model/hub/metrics.py +208 -0
- aiohomematic/model/hub/number.py +42 -0
- aiohomematic/model/hub/select.py +52 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +43 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/hub/update.py +221 -0
- aiohomematic/model/support.py +592 -0
- aiohomematic/model/update.py +140 -0
- aiohomematic/model/week_profile.py +1827 -0
- aiohomematic/property_decorators.py +719 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
- aiohomematic/rega_scripts/create_backup_start.fn +28 -0
- aiohomematic/rega_scripts/create_backup_status.fn +89 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
- aiohomematic/rega_scripts/get_backend_info.fn +25 -0
- aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_service_messages.fn +83 -0
- aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
- aiohomematic/rega_scripts/set_program_state.fn +17 -0
- aiohomematic/rega_scripts/set_system_variable.fn +19 -0
- aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
- aiohomematic/schemas.py +256 -0
- aiohomematic/store/__init__.py +55 -0
- aiohomematic/store/dynamic/__init__.py +43 -0
- aiohomematic/store/dynamic/command.py +250 -0
- aiohomematic/store/dynamic/data.py +175 -0
- aiohomematic/store/dynamic/details.py +187 -0
- aiohomematic/store/dynamic/ping_pong.py +416 -0
- aiohomematic/store/persistent/__init__.py +71 -0
- aiohomematic/store/persistent/base.py +285 -0
- aiohomematic/store/persistent/device.py +233 -0
- aiohomematic/store/persistent/incident.py +380 -0
- aiohomematic/store/persistent/paramset.py +241 -0
- aiohomematic/store/persistent/session.py +556 -0
- aiohomematic/store/serialization.py +150 -0
- aiohomematic/store/storage.py +689 -0
- aiohomematic/store/types.py +526 -0
- aiohomematic/store/visibility/__init__.py +40 -0
- aiohomematic/store/visibility/parser.py +141 -0
- aiohomematic/store/visibility/registry.py +722 -0
- aiohomematic/store/visibility/rules.py +307 -0
- aiohomematic/strings.json +237 -0
- aiohomematic/support.py +706 -0
- aiohomematic/tracing.py +236 -0
- aiohomematic/translations/de.json +237 -0
- aiohomematic/translations/en.json +237 -0
- aiohomematic/type_aliases.py +51 -0
- aiohomematic/validator.py +128 -0
- aiohomematic-2026.1.29.dist-info/METADATA +296 -0
- aiohomematic-2026.1.29.dist-info/RECORD +188 -0
- aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
- aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
- aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
- aiohomematic-2026.1.29.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Custom cover data points for blinds, shutters, and garage doors.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from enum import IntEnum, StrEnum
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Final, Unpack
|
|
15
|
+
|
|
16
|
+
from aiohomematic.const import DataPointCategory, DataPointUsage, DeviceProfile, Field, Parameter
|
|
17
|
+
from aiohomematic.converter import convert_hm_level_to_cpv
|
|
18
|
+
from aiohomematic.model.custom.data_point import CustomDataPoint
|
|
19
|
+
from aiohomematic.model.custom.field import DataPointField
|
|
20
|
+
from aiohomematic.model.custom.mixins import PositionMixin, StateChangeArgs
|
|
21
|
+
from aiohomematic.model.custom.registry import DeviceProfileRegistry, ExtendedDeviceConfig
|
|
22
|
+
from aiohomematic.model.data_point import CallParameterCollector, bind_collector
|
|
23
|
+
from aiohomematic.model.generic import DpAction, DpActionSelect, DpFloat, DpSelect, DpSensor
|
|
24
|
+
from aiohomematic.property_decorators import state_property
|
|
25
|
+
|
|
26
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Timeout for acquiring the per-instance command processing lock to avoid
|
|
29
|
+
# potential deadlocks or indefinite serialization if an awaited call inside
|
|
30
|
+
# the critical section stalls.
|
|
31
|
+
_COMMAND_LOCK_TIMEOUT: Final[float] = 5.0
|
|
32
|
+
|
|
33
|
+
_CLOSED_LEVEL: Final = 0.0
|
|
34
|
+
_COVER_VENT_MAX_POSITION: Final = 50
|
|
35
|
+
_LEVEL_TO_POSITION_MULTIPLIER: Final = 100.0
|
|
36
|
+
_MAX_LEVEL_POSITION: Final = 100.0
|
|
37
|
+
_MIN_LEVEL_POSITION: Final = 0.0
|
|
38
|
+
_OPEN_LEVEL: Final = 1.0
|
|
39
|
+
_OPEN_TILT_LEVEL: Final = 1.0
|
|
40
|
+
_WD_CLOSED_LEVEL: Final = -0.005
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _CoverActivity(StrEnum):
|
|
44
|
+
"""Enum with cover activities."""
|
|
45
|
+
|
|
46
|
+
CLOSING = "DOWN"
|
|
47
|
+
OPENING = "UP"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _CoverPosition(IntEnum):
|
|
51
|
+
"""Enum with cover positions."""
|
|
52
|
+
|
|
53
|
+
OPEN = 100
|
|
54
|
+
VENT = 10
|
|
55
|
+
CLOSED = 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _GarageDoorActivity(IntEnum):
|
|
59
|
+
"""Enum with garage door commands."""
|
|
60
|
+
|
|
61
|
+
CLOSING = 5
|
|
62
|
+
OPENING = 2
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _GarageDoorCommand(StrEnum):
|
|
66
|
+
"""Enum with garage door commands."""
|
|
67
|
+
|
|
68
|
+
CLOSE = "CLOSE"
|
|
69
|
+
NOP = "NOP"
|
|
70
|
+
OPEN = "OPEN"
|
|
71
|
+
PARTIAL_OPEN = "PARTIAL_OPEN"
|
|
72
|
+
STOP = "STOP"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _GarageDoorState(StrEnum):
|
|
76
|
+
"""Enum with garage door states."""
|
|
77
|
+
|
|
78
|
+
CLOSED = "CLOSED"
|
|
79
|
+
OPEN = "OPEN"
|
|
80
|
+
VENTILATION_POSITION = "VENTILATION_POSITION"
|
|
81
|
+
POSITION_UNKNOWN = "_POSITION_UNKNOWN"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _StateChangeArg(StrEnum):
|
|
85
|
+
"""Enum with cover state change arguments."""
|
|
86
|
+
|
|
87
|
+
CLOSE = "close"
|
|
88
|
+
OPEN = "open"
|
|
89
|
+
POSITION = "position"
|
|
90
|
+
TILT_CLOSE = "tilt_close"
|
|
91
|
+
TILT_OPEN = "tilt_open"
|
|
92
|
+
TILT_POSITION = "tilt_position"
|
|
93
|
+
VENT = "vent"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CustomDpCover(PositionMixin, CustomDataPoint):
|
|
97
|
+
"""Class for Homematic cover data point."""
|
|
98
|
+
|
|
99
|
+
__slots__ = (
|
|
100
|
+
"_command_processing_lock",
|
|
101
|
+
"_use_group_channel_for_cover_state",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
_category = DataPointCategory.COVER
|
|
105
|
+
_closed_level: float = _CLOSED_LEVEL
|
|
106
|
+
_closed_position: int = int(_CLOSED_LEVEL * _LEVEL_TO_POSITION_MULTIPLIER)
|
|
107
|
+
_open_level: float = _OPEN_LEVEL
|
|
108
|
+
|
|
109
|
+
# Declarative data point field definitions
|
|
110
|
+
_dp_direction: Final = DataPointField(field=Field.DIRECTION, dpt=DpSensor[str | None])
|
|
111
|
+
_dp_group_level: Final = DataPointField(field=Field.GROUP_LEVEL, dpt=DpSensor[float | None])
|
|
112
|
+
_dp_level: Final = DataPointField(field=Field.LEVEL, dpt=DpFloat)
|
|
113
|
+
_dp_stop: Final = DataPointField(field=Field.STOP, dpt=DpAction)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def _group_level(self) -> float:
|
|
117
|
+
"""Return the channel level of the cover."""
|
|
118
|
+
if (
|
|
119
|
+
self._use_group_channel_for_cover_state
|
|
120
|
+
and self._dp_group_level.value is not None
|
|
121
|
+
and self.usage == DataPointUsage.CDP_PRIMARY
|
|
122
|
+
):
|
|
123
|
+
return float(self._dp_group_level.value)
|
|
124
|
+
return self._dp_level.value if self._dp_level.value is not None else self._closed_level
|
|
125
|
+
|
|
126
|
+
@state_property
|
|
127
|
+
def current_channel_position(self) -> int:
|
|
128
|
+
"""Return current channel position of cover."""
|
|
129
|
+
return self.level_to_position(self._dp_level.value) or self._closed_position
|
|
130
|
+
|
|
131
|
+
@state_property
|
|
132
|
+
def current_position(self) -> int:
|
|
133
|
+
"""Return current group position of cover."""
|
|
134
|
+
return self.level_to_position(self._group_level) or self._closed_position
|
|
135
|
+
|
|
136
|
+
@state_property
|
|
137
|
+
def is_closed(self) -> bool:
|
|
138
|
+
"""Return if the cover is closed."""
|
|
139
|
+
return self._group_level == self._closed_level
|
|
140
|
+
|
|
141
|
+
@state_property
|
|
142
|
+
def is_closing(self) -> bool | None:
|
|
143
|
+
"""Return if the cover is closing."""
|
|
144
|
+
if self._dp_direction.value is not None:
|
|
145
|
+
return str(self._dp_direction.value) == _CoverActivity.CLOSING
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
@state_property
|
|
149
|
+
def is_opening(self) -> bool | None:
|
|
150
|
+
"""Return if the cover is opening."""
|
|
151
|
+
if self._dp_direction.value is not None:
|
|
152
|
+
return str(self._dp_direction.value) == _CoverActivity.OPENING
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
@bind_collector
|
|
156
|
+
async def close(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
157
|
+
"""Close the cover."""
|
|
158
|
+
if not self.is_state_change(close=True):
|
|
159
|
+
return
|
|
160
|
+
await self._set_level(level=self._closed_level, collector=collector)
|
|
161
|
+
|
|
162
|
+
def is_state_change(self, **kwargs: Unpack[StateChangeArgs]) -> bool:
|
|
163
|
+
"""Check if the state changes due to kwargs."""
|
|
164
|
+
if kwargs.get(_StateChangeArg.OPEN) is not None and self._group_level != self._open_level:
|
|
165
|
+
return True
|
|
166
|
+
if kwargs.get(_StateChangeArg.CLOSE) is not None and self._group_level != self._closed_level:
|
|
167
|
+
return True
|
|
168
|
+
if (position := kwargs.get(_StateChangeArg.POSITION)) is not None and position != self.current_position:
|
|
169
|
+
return True
|
|
170
|
+
return super().is_state_change(**kwargs)
|
|
171
|
+
|
|
172
|
+
@bind_collector
|
|
173
|
+
async def open(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
174
|
+
"""Open the cover."""
|
|
175
|
+
if not self.is_state_change(open=True):
|
|
176
|
+
return
|
|
177
|
+
await self._set_level(level=self._open_level, collector=collector)
|
|
178
|
+
|
|
179
|
+
@bind_collector
|
|
180
|
+
async def set_position(
|
|
181
|
+
self,
|
|
182
|
+
*,
|
|
183
|
+
position: int | None = None,
|
|
184
|
+
tilt_position: int | None = None,
|
|
185
|
+
collector: CallParameterCollector | None = None,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Move the cover to a specific position."""
|
|
188
|
+
if not self.is_state_change(position=position):
|
|
189
|
+
return
|
|
190
|
+
level = (
|
|
191
|
+
self.position_to_level(int(min(_MAX_LEVEL_POSITION, max(_MIN_LEVEL_POSITION, position))))
|
|
192
|
+
if position is not None
|
|
193
|
+
else None
|
|
194
|
+
)
|
|
195
|
+
await self._set_level(level=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 _post_init(self) -> None:
|
|
203
|
+
"""Post action after initialisation of the data point fields."""
|
|
204
|
+
super()._post_init()
|
|
205
|
+
|
|
206
|
+
self._command_processing_lock = asyncio.Lock()
|
|
207
|
+
self._use_group_channel_for_cover_state = self._device.config_provider.config.use_group_channel_for_cover_state
|
|
208
|
+
|
|
209
|
+
async def _set_level(
|
|
210
|
+
self,
|
|
211
|
+
*,
|
|
212
|
+
level: float | None = None,
|
|
213
|
+
tilt_level: float | None = None,
|
|
214
|
+
collector: CallParameterCollector | None = None,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Move the cover to a specific position. Value range is 0.0 to 1.01."""
|
|
217
|
+
if level is None:
|
|
218
|
+
return
|
|
219
|
+
await self._dp_level.send_value(value=level, collector=collector)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class CustomDpWindowDrive(CustomDpCover):
|
|
223
|
+
"""Class for Homematic window drive."""
|
|
224
|
+
|
|
225
|
+
__slots__ = ()
|
|
226
|
+
|
|
227
|
+
_closed_level: float = _WD_CLOSED_LEVEL
|
|
228
|
+
_open_level: float = _OPEN_LEVEL
|
|
229
|
+
|
|
230
|
+
@state_property
|
|
231
|
+
def current_position(self) -> int:
|
|
232
|
+
"""Return current position of cover."""
|
|
233
|
+
level = self._dp_level.value if self._dp_level.value is not None else self._closed_level
|
|
234
|
+
if level == _WD_CLOSED_LEVEL:
|
|
235
|
+
level = _CLOSED_LEVEL
|
|
236
|
+
elif level == _CLOSED_LEVEL:
|
|
237
|
+
level = 0.01
|
|
238
|
+
return self.level_to_position(level) or self._closed_position
|
|
239
|
+
|
|
240
|
+
async def _set_level(
|
|
241
|
+
self,
|
|
242
|
+
*,
|
|
243
|
+
level: float | None = None,
|
|
244
|
+
tilt_level: float | None = None,
|
|
245
|
+
collector: CallParameterCollector | None = None,
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Move the window drive to a specific position. Value range is -0.005 to 1.01."""
|
|
248
|
+
if level is None:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
if level == _CLOSED_LEVEL:
|
|
252
|
+
wd_level = _WD_CLOSED_LEVEL
|
|
253
|
+
elif _CLOSED_LEVEL < level <= 0.01:
|
|
254
|
+
wd_level = 0
|
|
255
|
+
else:
|
|
256
|
+
wd_level = level
|
|
257
|
+
await self._dp_level.send_value(value=wd_level, collector=collector, do_validate=False)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class CustomDpBlind(CustomDpCover):
|
|
261
|
+
"""Class for Homematic blind data point."""
|
|
262
|
+
|
|
263
|
+
__slots__ = () # Required to prevent __dict__ creation (descriptors are class-level)
|
|
264
|
+
|
|
265
|
+
_open_tilt_level: float = _OPEN_TILT_LEVEL
|
|
266
|
+
|
|
267
|
+
# Declarative data point field definitions
|
|
268
|
+
_dp_combined = DataPointField(field=Field.LEVEL_COMBINED, dpt=DpAction)
|
|
269
|
+
_dp_group_level_2: Final = DataPointField(field=Field.GROUP_LEVEL_2, dpt=DpSensor[float | None])
|
|
270
|
+
_dp_level_2: Final = DataPointField(field=Field.LEVEL_2, dpt=DpFloat)
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def _group_tilt_level(self) -> float:
|
|
274
|
+
"""Return the group level of the tilt."""
|
|
275
|
+
if (
|
|
276
|
+
self._use_group_channel_for_cover_state
|
|
277
|
+
and self._dp_group_level_2.value is not None
|
|
278
|
+
and self.usage == DataPointUsage.CDP_PRIMARY
|
|
279
|
+
):
|
|
280
|
+
return float(self._dp_group_level_2.value)
|
|
281
|
+
return self._dp_level_2.value if self._dp_level_2.value is not None else self._closed_level
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def _target_level(self) -> float | None:
|
|
285
|
+
"""Return the level of last service call."""
|
|
286
|
+
if (last_value_send := self._dp_level.unconfirmed_last_value_send) is not None:
|
|
287
|
+
return float(last_value_send)
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def _target_tilt_level(self) -> float | None:
|
|
292
|
+
"""Return the tilt level of last service call."""
|
|
293
|
+
if (last_value_send := self._dp_level_2.unconfirmed_last_value_send) is not None:
|
|
294
|
+
return float(last_value_send)
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
@state_property
|
|
298
|
+
def current_channel_tilt_position(self) -> int:
|
|
299
|
+
"""Return current channel_tilt position of cover."""
|
|
300
|
+
return self.level_to_position(self._dp_level_2.value) or self._closed_position
|
|
301
|
+
|
|
302
|
+
@state_property
|
|
303
|
+
def current_tilt_position(self) -> int:
|
|
304
|
+
"""Return current tilt position of cover."""
|
|
305
|
+
return self.level_to_position(self._group_tilt_level) or self._closed_position
|
|
306
|
+
|
|
307
|
+
@bind_collector(enabled=False)
|
|
308
|
+
async def close(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
309
|
+
"""Close the cover and close the tilt."""
|
|
310
|
+
if not self.is_state_change(close=True, tilt_close=True):
|
|
311
|
+
return
|
|
312
|
+
await self._set_level(
|
|
313
|
+
level=self._closed_level,
|
|
314
|
+
tilt_level=self._closed_level,
|
|
315
|
+
collector=collector,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
@bind_collector(enabled=False)
|
|
319
|
+
async def close_tilt(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
320
|
+
"""Close the tilt."""
|
|
321
|
+
if not self.is_state_change(tilt_close=True):
|
|
322
|
+
return
|
|
323
|
+
await self._set_level(tilt_level=self._closed_level, collector=collector)
|
|
324
|
+
|
|
325
|
+
def is_state_change(self, **kwargs: Unpack[StateChangeArgs]) -> bool:
|
|
326
|
+
"""Check if the state changes due to kwargs."""
|
|
327
|
+
if (
|
|
328
|
+
tilt_position := kwargs.get(_StateChangeArg.TILT_POSITION)
|
|
329
|
+
) is not None and tilt_position != self.current_tilt_position:
|
|
330
|
+
return True
|
|
331
|
+
if kwargs.get(_StateChangeArg.TILT_OPEN) is not None and self.current_tilt_position != _CoverPosition.OPEN:
|
|
332
|
+
return True
|
|
333
|
+
if kwargs.get(_StateChangeArg.TILT_CLOSE) is not None and self.current_tilt_position != _CoverPosition.CLOSED:
|
|
334
|
+
return True
|
|
335
|
+
return super().is_state_change(**kwargs)
|
|
336
|
+
|
|
337
|
+
@bind_collector(enabled=False)
|
|
338
|
+
async def open(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
339
|
+
"""Open the cover and open the tilt."""
|
|
340
|
+
if not self.is_state_change(open=True, tilt_open=True):
|
|
341
|
+
return
|
|
342
|
+
await self._set_level(
|
|
343
|
+
level=self._open_level,
|
|
344
|
+
tilt_level=self._open_tilt_level,
|
|
345
|
+
collector=collector,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
@bind_collector(enabled=False)
|
|
349
|
+
async def open_tilt(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
350
|
+
"""Open the tilt."""
|
|
351
|
+
if not self.is_state_change(tilt_open=True):
|
|
352
|
+
return
|
|
353
|
+
await self._set_level(tilt_level=self._open_tilt_level, collector=collector)
|
|
354
|
+
|
|
355
|
+
@bind_collector(enabled=False)
|
|
356
|
+
async def set_position(
|
|
357
|
+
self,
|
|
358
|
+
*,
|
|
359
|
+
position: int | None = None,
|
|
360
|
+
tilt_position: int | None = None,
|
|
361
|
+
collector: CallParameterCollector | None = None,
|
|
362
|
+
) -> None:
|
|
363
|
+
"""Move the blind to a specific position."""
|
|
364
|
+
if not self.is_state_change(position=position, tilt_position=tilt_position):
|
|
365
|
+
return
|
|
366
|
+
level = (
|
|
367
|
+
self.position_to_level(int(min(_MAX_LEVEL_POSITION, max(_MIN_LEVEL_POSITION, position))))
|
|
368
|
+
if position is not None
|
|
369
|
+
else None
|
|
370
|
+
)
|
|
371
|
+
tilt_level = (
|
|
372
|
+
self.position_to_level(int(min(_MAX_LEVEL_POSITION, max(_MIN_LEVEL_POSITION, tilt_position))))
|
|
373
|
+
if tilt_position is not None
|
|
374
|
+
else None
|
|
375
|
+
)
|
|
376
|
+
await self._set_level(level=level, tilt_level=tilt_level, collector=collector)
|
|
377
|
+
|
|
378
|
+
@bind_collector(enabled=False)
|
|
379
|
+
async def stop(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
380
|
+
"""Stop the device if in motion."""
|
|
381
|
+
try:
|
|
382
|
+
acquired: bool = await asyncio.wait_for(
|
|
383
|
+
self._command_processing_lock.acquire(), timeout=_COMMAND_LOCK_TIMEOUT
|
|
384
|
+
)
|
|
385
|
+
except TimeoutError:
|
|
386
|
+
acquired = False
|
|
387
|
+
_LOGGER.warning( # i18n-log: ignore
|
|
388
|
+
"%s: command lock acquisition timed out; proceeding without lock", self
|
|
389
|
+
)
|
|
390
|
+
try:
|
|
391
|
+
await self._stop(collector=collector)
|
|
392
|
+
finally:
|
|
393
|
+
if acquired:
|
|
394
|
+
self._command_processing_lock.release()
|
|
395
|
+
|
|
396
|
+
@bind_collector(enabled=False)
|
|
397
|
+
async def stop_tilt(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
398
|
+
"""Stop the device if in motion. Use only when command_processing_lock is held."""
|
|
399
|
+
await self.stop(collector=collector)
|
|
400
|
+
|
|
401
|
+
def _get_combined_value(self, *, level: float | None = None, tilt_level: float | None = None) -> str | None:
|
|
402
|
+
"""Return the combined parameter."""
|
|
403
|
+
if level is None and tilt_level is None:
|
|
404
|
+
return None
|
|
405
|
+
levels: list[str] = []
|
|
406
|
+
# the resulting hex value is based on the doubled position
|
|
407
|
+
if level is not None:
|
|
408
|
+
levels.append(convert_hm_level_to_cpv(value=level))
|
|
409
|
+
if tilt_level is not None:
|
|
410
|
+
levels.append(convert_hm_level_to_cpv(value=tilt_level))
|
|
411
|
+
|
|
412
|
+
if levels:
|
|
413
|
+
return ",".join(levels)
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
@bind_collector
|
|
417
|
+
async def _send_level(
|
|
418
|
+
self,
|
|
419
|
+
*,
|
|
420
|
+
level: float,
|
|
421
|
+
tilt_level: float,
|
|
422
|
+
collector: CallParameterCollector | None = None,
|
|
423
|
+
) -> None:
|
|
424
|
+
"""Transmit a new target level to the device."""
|
|
425
|
+
if self._dp_combined.is_hmtype and (
|
|
426
|
+
combined_parameter := self._get_combined_value(level=level, tilt_level=tilt_level)
|
|
427
|
+
):
|
|
428
|
+
# don't use collector for blind combined parameter
|
|
429
|
+
await self._dp_combined.send_value(value=combined_parameter, collector=None)
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
await self._dp_level_2.send_value(value=tilt_level, collector=collector)
|
|
433
|
+
await super()._set_level(level=level, collector=collector)
|
|
434
|
+
|
|
435
|
+
async def _set_level(
|
|
436
|
+
self,
|
|
437
|
+
*,
|
|
438
|
+
level: float | None = None,
|
|
439
|
+
tilt_level: float | None = None,
|
|
440
|
+
collector: CallParameterCollector | None = None,
|
|
441
|
+
) -> None:
|
|
442
|
+
"""
|
|
443
|
+
Move the cover to a specific tilt level. Value range is 0.0 to 1.00.
|
|
444
|
+
|
|
445
|
+
level or tilt_level may be set to None for no change.
|
|
446
|
+
"""
|
|
447
|
+
currently_moving = False
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
acquired: bool = await asyncio.wait_for(
|
|
451
|
+
self._command_processing_lock.acquire(), timeout=_COMMAND_LOCK_TIMEOUT
|
|
452
|
+
)
|
|
453
|
+
except TimeoutError:
|
|
454
|
+
acquired = False
|
|
455
|
+
_LOGGER.warning( # i18n-log: ignore
|
|
456
|
+
"%s: command lock acquisition timed out; proceeding without lock", self
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
if level is not None:
|
|
461
|
+
_level = level
|
|
462
|
+
elif self._target_level is not None:
|
|
463
|
+
# The blind moves and the target blind height is known
|
|
464
|
+
currently_moving = True
|
|
465
|
+
_level = self._target_level
|
|
466
|
+
else: # The blind is at a standstill and no level is explicitly requested => we remain at the current level
|
|
467
|
+
_level = self._group_level
|
|
468
|
+
|
|
469
|
+
if tilt_level is not None:
|
|
470
|
+
_tilt_level = tilt_level
|
|
471
|
+
elif self._target_tilt_level is not None:
|
|
472
|
+
# The blind moves and the target slat position is known
|
|
473
|
+
currently_moving = True
|
|
474
|
+
_tilt_level = self._target_tilt_level
|
|
475
|
+
else: # The blind is at a standstill and no tilt is explicitly desired => we remain at the current angle
|
|
476
|
+
_tilt_level = self._group_tilt_level
|
|
477
|
+
|
|
478
|
+
if currently_moving:
|
|
479
|
+
# Blind actors are buggy when sending new coordinates while they are moving. So we stop them first.
|
|
480
|
+
await self._stop()
|
|
481
|
+
|
|
482
|
+
await self._send_level(level=_level, tilt_level=_tilt_level, collector=collector)
|
|
483
|
+
finally:
|
|
484
|
+
if acquired:
|
|
485
|
+
self._command_processing_lock.release()
|
|
486
|
+
|
|
487
|
+
@bind_collector(enabled=False)
|
|
488
|
+
async def _stop(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
489
|
+
"""Stop the device if in motion. Do only call with _command_processing_lock held."""
|
|
490
|
+
await super().stop(collector=collector)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class CustomDpIpBlind(CustomDpBlind):
|
|
494
|
+
"""Class for HomematicIP blind data point."""
|
|
495
|
+
|
|
496
|
+
__slots__ = () # Required to prevent __dict__ creation (descriptors are class-level)
|
|
497
|
+
|
|
498
|
+
# Declarative data point field definitions (override parent)
|
|
499
|
+
_dp_combined = DataPointField(field=Field.COMBINED_PARAMETER, dpt=DpAction)
|
|
500
|
+
_dp_operation_mode: Final = DataPointField(field=Field.OPERATION_MODE, dpt=DpSelect)
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def operation_mode(self) -> str | None:
|
|
504
|
+
"""Return operation mode of cover."""
|
|
505
|
+
val = self._dp_operation_mode.value
|
|
506
|
+
return val if isinstance(val, str) else None
|
|
507
|
+
|
|
508
|
+
def _get_combined_value(self, *, level: float | None = None, tilt_level: float | None = None) -> str | None:
|
|
509
|
+
"""Return the combined parameter."""
|
|
510
|
+
if level is None and tilt_level is None:
|
|
511
|
+
return None
|
|
512
|
+
levels: list[str] = []
|
|
513
|
+
if (tilt_pos := self.level_to_position(tilt_level)) is not None:
|
|
514
|
+
levels.append(f"L2={tilt_pos}")
|
|
515
|
+
if (level_pos := self.level_to_position(level)) is not None:
|
|
516
|
+
levels.append(f"L={level_pos}")
|
|
517
|
+
|
|
518
|
+
if levels:
|
|
519
|
+
return ",".join(levels)
|
|
520
|
+
return None
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class CustomDpGarage(PositionMixin, CustomDataPoint):
|
|
524
|
+
"""Class for Homematic garage data point."""
|
|
525
|
+
|
|
526
|
+
__slots__ = () # Required to prevent __dict__ creation (descriptors are class-level)
|
|
527
|
+
|
|
528
|
+
_category = DataPointCategory.COVER
|
|
529
|
+
|
|
530
|
+
# Declarative data point field definitions
|
|
531
|
+
_dp_door_command: Final = DataPointField(field=Field.DOOR_COMMAND, dpt=DpActionSelect)
|
|
532
|
+
_dp_door_state: Final = DataPointField(field=Field.DOOR_STATE, dpt=DpSensor[str | None])
|
|
533
|
+
_dp_section: Final = DataPointField(field=Field.SECTION, dpt=DpSensor[int | None])
|
|
534
|
+
|
|
535
|
+
@state_property
|
|
536
|
+
def current_position(self) -> int | None:
|
|
537
|
+
"""Return current position of the garage door ."""
|
|
538
|
+
if self._dp_door_state.value == _GarageDoorState.OPEN:
|
|
539
|
+
return _CoverPosition.OPEN
|
|
540
|
+
if self._dp_door_state.value == _GarageDoorState.VENTILATION_POSITION:
|
|
541
|
+
return _CoverPosition.VENT
|
|
542
|
+
if self._dp_door_state.value == _GarageDoorState.CLOSED:
|
|
543
|
+
return _CoverPosition.CLOSED
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
@state_property
|
|
547
|
+
def is_closed(self) -> bool | None:
|
|
548
|
+
"""Return if the garage door is closed."""
|
|
549
|
+
if self._dp_door_state.value is not None:
|
|
550
|
+
return str(self._dp_door_state.value) == _GarageDoorState.CLOSED
|
|
551
|
+
return None
|
|
552
|
+
|
|
553
|
+
@state_property
|
|
554
|
+
def is_closing(self) -> bool | None:
|
|
555
|
+
"""Return if the garage door is closing."""
|
|
556
|
+
if self._dp_section.value is not None:
|
|
557
|
+
return int(self._dp_section.value) == _GarageDoorActivity.CLOSING
|
|
558
|
+
return None
|
|
559
|
+
|
|
560
|
+
@state_property
|
|
561
|
+
def is_opening(self) -> bool | None:
|
|
562
|
+
"""Return if the garage door is opening."""
|
|
563
|
+
if self._dp_section.value is not None:
|
|
564
|
+
return int(self._dp_section.value) == _GarageDoorActivity.OPENING
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
@bind_collector
|
|
568
|
+
async def close(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
569
|
+
"""Close the garage door."""
|
|
570
|
+
if not self.is_state_change(close=True):
|
|
571
|
+
return
|
|
572
|
+
await self._dp_door_command.send_value(value=_GarageDoorCommand.CLOSE, collector=collector)
|
|
573
|
+
|
|
574
|
+
def is_state_change(self, **kwargs: Unpack[StateChangeArgs]) -> bool:
|
|
575
|
+
"""Check if the state changes due to kwargs."""
|
|
576
|
+
if kwargs.get(_StateChangeArg.OPEN) is not None and self.current_position != _CoverPosition.OPEN:
|
|
577
|
+
return True
|
|
578
|
+
if kwargs.get(_StateChangeArg.VENT) is not None and self.current_position != _CoverPosition.VENT:
|
|
579
|
+
return True
|
|
580
|
+
if kwargs.get(_StateChangeArg.CLOSE) is not None and self.current_position != _CoverPosition.CLOSED:
|
|
581
|
+
return True
|
|
582
|
+
return super().is_state_change(**kwargs)
|
|
583
|
+
|
|
584
|
+
@bind_collector
|
|
585
|
+
async def open(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
586
|
+
"""Open the garage door."""
|
|
587
|
+
if not self.is_state_change(open=True):
|
|
588
|
+
return
|
|
589
|
+
await self._dp_door_command.send_value(value=_GarageDoorCommand.OPEN, collector=collector)
|
|
590
|
+
|
|
591
|
+
@bind_collector
|
|
592
|
+
async def set_position(
|
|
593
|
+
self,
|
|
594
|
+
*,
|
|
595
|
+
position: int | None = None,
|
|
596
|
+
tilt_position: int | None = None,
|
|
597
|
+
collector: CallParameterCollector | None = None,
|
|
598
|
+
) -> None:
|
|
599
|
+
"""Move the garage door to a specific position."""
|
|
600
|
+
if position is None:
|
|
601
|
+
return
|
|
602
|
+
if _COVER_VENT_MAX_POSITION < position <= _CoverPosition.OPEN:
|
|
603
|
+
await self.open(collector=collector)
|
|
604
|
+
if _CoverPosition.VENT < position <= _COVER_VENT_MAX_POSITION:
|
|
605
|
+
await self.vent(collector=collector)
|
|
606
|
+
if _CoverPosition.CLOSED <= position <= _CoverPosition.VENT:
|
|
607
|
+
await self.close(collector=collector)
|
|
608
|
+
|
|
609
|
+
@bind_collector(enabled=False)
|
|
610
|
+
async def stop(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
611
|
+
"""Stop the device if in motion."""
|
|
612
|
+
await self._dp_door_command.send_value(value=_GarageDoorCommand.STOP, collector=collector)
|
|
613
|
+
|
|
614
|
+
@bind_collector
|
|
615
|
+
async def vent(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
616
|
+
"""Move the garage door to vent position."""
|
|
617
|
+
if not self.is_state_change(vent=True):
|
|
618
|
+
return
|
|
619
|
+
await self._dp_door_command.send_value(value=_GarageDoorCommand.PARTIAL_OPEN, collector=collector)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# =============================================================================
|
|
623
|
+
# DeviceProfileRegistry Registration
|
|
624
|
+
# =============================================================================
|
|
625
|
+
|
|
626
|
+
# RF Cover
|
|
627
|
+
DeviceProfileRegistry.register(
|
|
628
|
+
category=DataPointCategory.COVER,
|
|
629
|
+
models=(
|
|
630
|
+
"263 146",
|
|
631
|
+
"263 147",
|
|
632
|
+
"HM-LC-Bl1-Velux",
|
|
633
|
+
"HM-LC-Bl1-FM",
|
|
634
|
+
"HM-LC-Bl1-FM-2",
|
|
635
|
+
"HM-LC-Bl1-PB-FM",
|
|
636
|
+
"HM-LC-Bl1-SM",
|
|
637
|
+
"HM-LC-Bl1-SM-2",
|
|
638
|
+
"HM-LC-Bl1PBU-FM",
|
|
639
|
+
"HM-LC-BlX",
|
|
640
|
+
"ZEL STG RM FEP 230V",
|
|
641
|
+
),
|
|
642
|
+
data_point_class=CustomDpCover,
|
|
643
|
+
profile_type=DeviceProfile.RF_COVER,
|
|
644
|
+
)
|
|
645
|
+
DeviceProfileRegistry.register(
|
|
646
|
+
category=DataPointCategory.COVER,
|
|
647
|
+
models="HMW-LC-Bl1",
|
|
648
|
+
data_point_class=CustomDpCover,
|
|
649
|
+
profile_type=DeviceProfile.RF_COVER,
|
|
650
|
+
channels=(3,),
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
# RF Blind
|
|
654
|
+
DeviceProfileRegistry.register(
|
|
655
|
+
category=DataPointCategory.COVER,
|
|
656
|
+
models=("HM-LC-Ja1PBU-FM", "HM-LC-JaX"),
|
|
657
|
+
data_point_class=CustomDpBlind,
|
|
658
|
+
profile_type=DeviceProfile.RF_COVER,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
# RF Window Drive
|
|
662
|
+
DeviceProfileRegistry.register(
|
|
663
|
+
category=DataPointCategory.COVER,
|
|
664
|
+
models="HM-Sec-Win",
|
|
665
|
+
data_point_class=CustomDpWindowDrive,
|
|
666
|
+
profile_type=DeviceProfile.RF_COVER,
|
|
667
|
+
channels=(1,),
|
|
668
|
+
extended=ExtendedDeviceConfig(
|
|
669
|
+
additional_data_points={
|
|
670
|
+
1: (Parameter.DIRECTION, Parameter.WORKING, Parameter.ERROR),
|
|
671
|
+
2: (Parameter.LEVEL, Parameter.STATUS),
|
|
672
|
+
}
|
|
673
|
+
),
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
# IP Cover
|
|
677
|
+
DeviceProfileRegistry.register(
|
|
678
|
+
category=DataPointCategory.COVER,
|
|
679
|
+
models=("HmIP-BROLL", "HmIP-FROLL"),
|
|
680
|
+
data_point_class=CustomDpCover,
|
|
681
|
+
profile_type=DeviceProfile.IP_COVER,
|
|
682
|
+
channels=(4,),
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# IP Blind
|
|
686
|
+
DeviceProfileRegistry.register(
|
|
687
|
+
category=DataPointCategory.COVER,
|
|
688
|
+
models=("HmIP-BBL", "HmIP-FBL"),
|
|
689
|
+
data_point_class=CustomDpIpBlind,
|
|
690
|
+
profile_type=DeviceProfile.IP_COVER,
|
|
691
|
+
channels=(4,),
|
|
692
|
+
)
|
|
693
|
+
DeviceProfileRegistry.register(
|
|
694
|
+
category=DataPointCategory.COVER,
|
|
695
|
+
models="HmIP-DRBLI4",
|
|
696
|
+
data_point_class=CustomDpIpBlind,
|
|
697
|
+
profile_type=DeviceProfile.IP_COVER,
|
|
698
|
+
channels=(10, 14, 18, 22),
|
|
699
|
+
)
|
|
700
|
+
DeviceProfileRegistry.register(
|
|
701
|
+
category=DataPointCategory.COVER,
|
|
702
|
+
models="HmIPW-DRBL4",
|
|
703
|
+
data_point_class=CustomDpIpBlind,
|
|
704
|
+
profile_type=DeviceProfile.IP_COVER,
|
|
705
|
+
channels=(2, 6, 10, 14),
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
# IP HDM
|
|
709
|
+
DeviceProfileRegistry.register(
|
|
710
|
+
category=DataPointCategory.COVER,
|
|
711
|
+
models="HmIP-HDM",
|
|
712
|
+
data_point_class=CustomDpIpBlind,
|
|
713
|
+
profile_type=DeviceProfile.IP_HDM,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# IP Garage
|
|
717
|
+
DeviceProfileRegistry.register(
|
|
718
|
+
category=DataPointCategory.COVER,
|
|
719
|
+
models=("HmIP-MOD-HO", "HmIP-MOD-TM"),
|
|
720
|
+
data_point_class=CustomDpGarage,
|
|
721
|
+
profile_type=DeviceProfile.IP_GARAGE,
|
|
722
|
+
)
|