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,216 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Event model for AioHomematic.
|
|
5
|
+
|
|
6
|
+
This module defines the event data point hierarchy used to expose Homematic
|
|
7
|
+
button presses, device errors, and impulse notifications to applications.
|
|
8
|
+
|
|
9
|
+
Included classes:
|
|
10
|
+
- GenericEvent: Base event that integrates with the common data point API
|
|
11
|
+
(category, usage, names/paths, subscriptions) and provides publish_event handling.
|
|
12
|
+
- ClickEvent: Represents key press events (EventType.KEYPRESS).
|
|
13
|
+
- DeviceErrorEvent: Represents device error signaling with special value change
|
|
14
|
+
semantics before publishing an event (EventType.DEVICE_ERROR).
|
|
15
|
+
- ImpulseEvent: Represents impulse events (EventType.IMPULSE).
|
|
16
|
+
|
|
17
|
+
Factory helpers:
|
|
18
|
+
- create_event_and_append_to_channel: Determines the appropriate event type for
|
|
19
|
+
a given parameter description and attaches an instance to the channel.
|
|
20
|
+
|
|
21
|
+
Typical flow:
|
|
22
|
+
1) During device initialization, model.create_data_points_and_events inspects
|
|
23
|
+
paramset descriptions.
|
|
24
|
+
2) For parameters that support Operations.EVENT and match known event names
|
|
25
|
+
(CLICK_EVENTS, DEVICE_ERROR_EVENTS, IMPULSE_EVENTS), an event data point is
|
|
26
|
+
created and registered on the channel.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from datetime import datetime
|
|
32
|
+
import logging
|
|
33
|
+
from typing import Any, Final
|
|
34
|
+
|
|
35
|
+
from aiohomematic import i18n, support as hms
|
|
36
|
+
from aiohomematic.async_support import loop_check
|
|
37
|
+
from aiohomematic.const import (
|
|
38
|
+
CLICK_EVENTS,
|
|
39
|
+
DATA_POINT_EVENTS,
|
|
40
|
+
DEVICE_ERROR_EVENTS,
|
|
41
|
+
IMPULSE_EVENTS,
|
|
42
|
+
DataPointCategory,
|
|
43
|
+
DataPointUsage,
|
|
44
|
+
DeviceTriggerEventType,
|
|
45
|
+
Operations,
|
|
46
|
+
ParameterData,
|
|
47
|
+
ParamsetKey,
|
|
48
|
+
ServiceScope,
|
|
49
|
+
)
|
|
50
|
+
from aiohomematic.decorators import inspector
|
|
51
|
+
from aiohomematic.exceptions import AioHomematicException
|
|
52
|
+
from aiohomematic.interfaces import ChannelProtocol, GenericEventProtocolAny
|
|
53
|
+
from aiohomematic.model.data_point import BaseParameterDataPointAny
|
|
54
|
+
from aiohomematic.model.support import DataPointNameData, get_event_name
|
|
55
|
+
from aiohomematic.property_decorators import DelegatedProperty
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"ClickEvent",
|
|
59
|
+
"DeviceErrorEvent",
|
|
60
|
+
"GenericEvent",
|
|
61
|
+
"ImpulseEvent",
|
|
62
|
+
"create_event_and_append_to_channel",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class GenericEvent(BaseParameterDataPointAny, GenericEventProtocolAny):
|
|
70
|
+
"""Base class for events."""
|
|
71
|
+
|
|
72
|
+
__slots__ = ("_device_trigger_event_type",)
|
|
73
|
+
|
|
74
|
+
_category = DataPointCategory.EVENT
|
|
75
|
+
_device_trigger_event_type: DeviceTriggerEventType
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
*,
|
|
80
|
+
channel: ChannelProtocol,
|
|
81
|
+
parameter: str,
|
|
82
|
+
parameter_data: ParameterData,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Initialize the event handler."""
|
|
85
|
+
super().__init__(
|
|
86
|
+
channel=channel,
|
|
87
|
+
paramset_key=ParamsetKey.VALUES,
|
|
88
|
+
parameter=parameter,
|
|
89
|
+
parameter_data=parameter_data,
|
|
90
|
+
unique_id_prefix=f"event_{channel.device.central_info.name}",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
event_type: Final = DelegatedProperty[DeviceTriggerEventType](path="_device_trigger_event_type")
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def usage(self) -> DataPointUsage:
|
|
97
|
+
"""Return the data_point usage."""
|
|
98
|
+
if (forced_by_com := self._enabled_by_channel_operation_mode) is None:
|
|
99
|
+
return self._get_data_point_usage()
|
|
100
|
+
return DataPointUsage.EVENT if forced_by_com else DataPointUsage.NO_CREATE
|
|
101
|
+
|
|
102
|
+
async def event(self, *, value: Any, received_at: datetime) -> None:
|
|
103
|
+
"""Handle event for which this handler has subscribed."""
|
|
104
|
+
if self.event_type in DATA_POINT_EVENTS:
|
|
105
|
+
self.publish_data_point_updated_event()
|
|
106
|
+
self._set_modified_at(modified_at=received_at)
|
|
107
|
+
self.publish_event(value=value)
|
|
108
|
+
|
|
109
|
+
@loop_check
|
|
110
|
+
def publish_event(self, *, value: Any) -> None:
|
|
111
|
+
"""Do what is needed to publish an event."""
|
|
112
|
+
self._event_publisher.publish_device_trigger_event(
|
|
113
|
+
trigger_type=self.event_type, event_data=self.get_event_data(value=value)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _get_data_point_name(self) -> DataPointNameData:
|
|
117
|
+
"""Create the name for the data_point."""
|
|
118
|
+
return get_event_name(
|
|
119
|
+
channel=self._channel,
|
|
120
|
+
parameter=self._parameter,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _get_data_point_usage(self) -> DataPointUsage:
|
|
124
|
+
"""Generate the usage for the data_point."""
|
|
125
|
+
return DataPointUsage.EVENT
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ClickEvent(GenericEvent):
|
|
129
|
+
"""class for handling click events."""
|
|
130
|
+
|
|
131
|
+
__slots__ = ()
|
|
132
|
+
|
|
133
|
+
_device_trigger_event_type = DeviceTriggerEventType.KEYPRESS
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class DeviceErrorEvent(GenericEvent):
|
|
137
|
+
"""class for handling device error events."""
|
|
138
|
+
|
|
139
|
+
__slots__ = ()
|
|
140
|
+
|
|
141
|
+
_device_trigger_event_type = DeviceTriggerEventType.DEVICE_ERROR
|
|
142
|
+
|
|
143
|
+
async def event(self, *, value: Any, received_at: datetime) -> None:
|
|
144
|
+
"""Handle event for which this handler has subscribed."""
|
|
145
|
+
old_value, new_value = self.write_value(value=value, write_at=received_at)
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
isinstance(new_value, bool)
|
|
149
|
+
and ((old_value is None and new_value is True) or (isinstance(old_value, bool) and old_value != new_value))
|
|
150
|
+
) or (
|
|
151
|
+
isinstance(new_value, int)
|
|
152
|
+
and ((old_value is None and new_value > 0) or (isinstance(old_value, int) and old_value != new_value))
|
|
153
|
+
):
|
|
154
|
+
self.publish_event(value=new_value)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ImpulseEvent(GenericEvent):
|
|
158
|
+
"""class for handling impulse events."""
|
|
159
|
+
|
|
160
|
+
__slots__ = ()
|
|
161
|
+
|
|
162
|
+
_device_trigger_event_type = DeviceTriggerEventType.IMPULSE
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@inspector(scope=ServiceScope.INTERNAL)
|
|
166
|
+
def create_event_and_append_to_channel(
|
|
167
|
+
*, channel: ChannelProtocol, parameter: str, parameter_data: ParameterData
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Create action event data_point."""
|
|
170
|
+
_LOGGER.debug(
|
|
171
|
+
"CREATE_EVENT_AND_APPEND_TO_DEVICE: Creating event for %s, %s, %s",
|
|
172
|
+
channel.address,
|
|
173
|
+
parameter,
|
|
174
|
+
channel.device.interface_id,
|
|
175
|
+
)
|
|
176
|
+
if (event_t := _determine_event_type(parameter=parameter, parameter_data=parameter_data)) and (
|
|
177
|
+
event := _safe_create_event(
|
|
178
|
+
event_t=event_t, channel=channel, parameter=parameter, parameter_data=parameter_data
|
|
179
|
+
)
|
|
180
|
+
):
|
|
181
|
+
channel.add_data_point(data_point=event)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _determine_event_type(*, parameter: str, parameter_data: ParameterData) -> type[GenericEvent] | None:
|
|
185
|
+
event_t: type[GenericEvent] | None = None
|
|
186
|
+
if parameter_data["OPERATIONS"] & Operations.EVENT:
|
|
187
|
+
if parameter in CLICK_EVENTS:
|
|
188
|
+
event_t = ClickEvent
|
|
189
|
+
if parameter.startswith(DEVICE_ERROR_EVENTS):
|
|
190
|
+
event_t = DeviceErrorEvent
|
|
191
|
+
if parameter in IMPULSE_EVENTS:
|
|
192
|
+
event_t = ImpulseEvent
|
|
193
|
+
return event_t
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _safe_create_event(
|
|
197
|
+
*,
|
|
198
|
+
event_t: type[GenericEvent],
|
|
199
|
+
channel: ChannelProtocol,
|
|
200
|
+
parameter: str,
|
|
201
|
+
parameter_data: ParameterData,
|
|
202
|
+
) -> GenericEvent:
|
|
203
|
+
"""Safely create a event and handle exceptions."""
|
|
204
|
+
try:
|
|
205
|
+
return event_t(
|
|
206
|
+
channel=channel,
|
|
207
|
+
parameter=parameter,
|
|
208
|
+
parameter_data=parameter_data,
|
|
209
|
+
)
|
|
210
|
+
except Exception as exc:
|
|
211
|
+
raise AioHomematicException(
|
|
212
|
+
i18n.tr(
|
|
213
|
+
key="exception.model.event.create_event.failed",
|
|
214
|
+
reason=hms.extract_exc_args(exc=exc),
|
|
215
|
+
)
|
|
216
|
+
) from exc
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Generic data points for AioHomematic.
|
|
5
|
+
|
|
6
|
+
Overview
|
|
7
|
+
- This subpackage provides the default, device-agnostic data point classes
|
|
8
|
+
(switch, number, sensor, select, text, button, binary_sensor) used for most
|
|
9
|
+
parameters across Homematic devices.
|
|
10
|
+
- It also exposes a central factory function that selects the appropriate data
|
|
11
|
+
point class for a parameter based on its description provided by the backend.
|
|
12
|
+
|
|
13
|
+
Factory
|
|
14
|
+
- create_data_point_and_append_to_channel(channel, paramset_key, parameter, parameter_data)
|
|
15
|
+
inspects ParameterData (TYPE, OPERATIONS, FLAGS, etc.) to determine which
|
|
16
|
+
GenericDataPoint subclass to instantiate, creates it safely and appends it to
|
|
17
|
+
the given channel.
|
|
18
|
+
|
|
19
|
+
Mapping rules (simplified)
|
|
20
|
+
- TYPE==ACTION:
|
|
21
|
+
- OPERATIONS==WRITE -> DpButton (for specific button-like actions or virtual
|
|
22
|
+
remotes) else DpAction; otherwise, when also readable, treat as DpSwitch.
|
|
23
|
+
- TYPE in {BOOL, ENUM, FLOAT, INTEGER, STRING} with WRITE capabilities ->
|
|
24
|
+
DpSwitch, DpSelect, DpFloat, DpInteger, DpText respectively.
|
|
25
|
+
- Read-only parameters (no WRITE) become sensors; BOOL-like sensors are mapped
|
|
26
|
+
to DpBinarySensor when heuristics indicate binary semantics.
|
|
27
|
+
|
|
28
|
+
Special cases
|
|
29
|
+
- Virtual remote models and click parameters are recognized and mapped to
|
|
30
|
+
button-style data points.
|
|
31
|
+
- Certain device/parameter combinations may be wrapped into a different
|
|
32
|
+
category (e.g., switch shown as sensor) when the parameter is not meant to be
|
|
33
|
+
user-visible or is better represented as a sensor, depending on configuration
|
|
34
|
+
and device model.
|
|
35
|
+
|
|
36
|
+
Exports
|
|
37
|
+
- Generic data point base and concrete types: GenericDataPoint, DpSwitch,
|
|
38
|
+
DpAction, DpButton, DpBinarySensor, DpSelect, DpFloat, DpInteger, DpText,
|
|
39
|
+
DpSensor, BaseDpNumber.
|
|
40
|
+
- Factory: create_data_point_and_append_to_channel.
|
|
41
|
+
|
|
42
|
+
See Also
|
|
43
|
+
--------
|
|
44
|
+
- aiohomematic.model.custom: Custom data points for specific devices/features.
|
|
45
|
+
- aiohomematic.model.calculated: Calculated/derived data points.
|
|
46
|
+
- aiohomematic.model.device: Device and channel abstractions used here.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
from collections.abc import Mapping
|
|
53
|
+
import logging
|
|
54
|
+
from typing import Final
|
|
55
|
+
|
|
56
|
+
from aiohomematic import i18n, support as hms
|
|
57
|
+
from aiohomematic.const import (
|
|
58
|
+
CLICK_EVENTS,
|
|
59
|
+
VIRTUAL_REMOTE_MODELS,
|
|
60
|
+
Operations,
|
|
61
|
+
Parameter,
|
|
62
|
+
ParameterData,
|
|
63
|
+
ParameterType,
|
|
64
|
+
ParamsetKey,
|
|
65
|
+
ServiceScope,
|
|
66
|
+
)
|
|
67
|
+
from aiohomematic.decorators import inspector
|
|
68
|
+
from aiohomematic.exceptions import AioHomematicException
|
|
69
|
+
from aiohomematic.interfaces.model import ChannelProtocol, GenericDataPointProtocolAny
|
|
70
|
+
from aiohomematic.model.generic.action import DpAction
|
|
71
|
+
from aiohomematic.model.generic.action_select import DpActionSelect
|
|
72
|
+
from aiohomematic.model.generic.binary_sensor import DpBinarySensor
|
|
73
|
+
from aiohomematic.model.generic.button import DpButton
|
|
74
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint, GenericDataPointAny
|
|
75
|
+
from aiohomematic.model.generic.dummy import DpDummy
|
|
76
|
+
from aiohomematic.model.generic.number import BaseDpNumber, DpFloat, DpInteger
|
|
77
|
+
from aiohomematic.model.generic.select import DpSelect
|
|
78
|
+
from aiohomematic.model.generic.sensor import DpSensor
|
|
79
|
+
from aiohomematic.model.generic.switch import DpSwitch
|
|
80
|
+
from aiohomematic.model.generic.text import DpText
|
|
81
|
+
from aiohomematic.model.support import is_binary_sensor
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
# Base
|
|
85
|
+
"BaseDpNumber",
|
|
86
|
+
"GenericDataPoint",
|
|
87
|
+
"GenericDataPointAny",
|
|
88
|
+
# Data points
|
|
89
|
+
"DpAction",
|
|
90
|
+
"DpActionSelect",
|
|
91
|
+
"DpBinarySensor",
|
|
92
|
+
"DpButton",
|
|
93
|
+
"DpDummy",
|
|
94
|
+
"DpFloat",
|
|
95
|
+
"DpInteger",
|
|
96
|
+
"DpSelect",
|
|
97
|
+
"DpSensor",
|
|
98
|
+
"DpSwitch",
|
|
99
|
+
"DpText",
|
|
100
|
+
# Factory
|
|
101
|
+
"create_data_point_and_append_to_channel",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
105
|
+
_BUTTON_ACTIONS: Final[tuple[str, ...]] = ("RESET_MOTION", "RESET_PRESENCE")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DataPointTypeResolver:
|
|
109
|
+
"""
|
|
110
|
+
Resolver for determining data point types based on parameter characteristics.
|
|
111
|
+
|
|
112
|
+
Uses a lookup table strategy for extensible parameter type mapping.
|
|
113
|
+
This class centralizes the logic for determining which GenericDataPoint
|
|
114
|
+
subclass should be used for a given parameter.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
# Mapping of parameter types to data point classes for writable parameters
|
|
118
|
+
_WRITABLE_TYPE_MAP: Final[Mapping[ParameterType, type[GenericDataPointAny]]] = {
|
|
119
|
+
ParameterType.BOOL: DpSwitch,
|
|
120
|
+
ParameterType.ENUM: DpSelect,
|
|
121
|
+
ParameterType.FLOAT: DpFloat,
|
|
122
|
+
ParameterType.INTEGER: DpInteger,
|
|
123
|
+
ParameterType.STRING: DpText,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def _resolve_action(
|
|
128
|
+
cls,
|
|
129
|
+
*,
|
|
130
|
+
channel: ChannelProtocol,
|
|
131
|
+
parameter: str,
|
|
132
|
+
parameter_data: ParameterData,
|
|
133
|
+
p_operations: int,
|
|
134
|
+
) -> type[GenericDataPointAny]:
|
|
135
|
+
"""Resolve data point type for ACTION parameters."""
|
|
136
|
+
if p_operations == Operations.WRITE:
|
|
137
|
+
# Write-only action
|
|
138
|
+
if parameter in _BUTTON_ACTIONS or channel.device.model in VIRTUAL_REMOTE_MODELS:
|
|
139
|
+
return DpButton
|
|
140
|
+
# Write-only action with value_list -> DpActionSelect
|
|
141
|
+
if parameter_data.get("VALUE_LIST"):
|
|
142
|
+
return DpActionSelect
|
|
143
|
+
return DpAction
|
|
144
|
+
|
|
145
|
+
if parameter in CLICK_EVENTS:
|
|
146
|
+
return DpButton
|
|
147
|
+
|
|
148
|
+
# Read+write action treated as switch
|
|
149
|
+
return DpSwitch
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def _resolve_readonly(
|
|
153
|
+
cls,
|
|
154
|
+
*,
|
|
155
|
+
parameter: str,
|
|
156
|
+
parameter_data: ParameterData,
|
|
157
|
+
) -> type[GenericDataPointAny] | None:
|
|
158
|
+
"""Resolve data point type for read-only parameters."""
|
|
159
|
+
if parameter in CLICK_EVENTS:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
if is_binary_sensor(parameter_data=parameter_data):
|
|
163
|
+
parameter_data["TYPE"] = ParameterType.BOOL
|
|
164
|
+
return DpBinarySensor
|
|
165
|
+
|
|
166
|
+
return DpSensor
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def _resolve_writable(
|
|
170
|
+
cls,
|
|
171
|
+
*,
|
|
172
|
+
channel: ChannelProtocol,
|
|
173
|
+
parameter: str,
|
|
174
|
+
parameter_data: ParameterData,
|
|
175
|
+
p_type: ParameterType,
|
|
176
|
+
p_operations: int,
|
|
177
|
+
) -> type[GenericDataPointAny] | None:
|
|
178
|
+
"""Resolve data point type for writable parameters."""
|
|
179
|
+
# Handle ACTION type specially
|
|
180
|
+
if p_type == ParameterType.ACTION:
|
|
181
|
+
return cls._resolve_action(
|
|
182
|
+
channel=channel,
|
|
183
|
+
parameter=parameter,
|
|
184
|
+
parameter_data=parameter_data,
|
|
185
|
+
p_operations=p_operations,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Write-only non-ACTION parameters
|
|
189
|
+
if p_operations == Operations.WRITE:
|
|
190
|
+
# Write-only with value_list -> DpActionSelect
|
|
191
|
+
if parameter_data.get("VALUE_LIST"):
|
|
192
|
+
return DpActionSelect
|
|
193
|
+
return DpAction
|
|
194
|
+
|
|
195
|
+
# Use lookup table for standard types
|
|
196
|
+
return cls._WRITABLE_TYPE_MAP.get(p_type)
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def resolve(
|
|
200
|
+
cls,
|
|
201
|
+
*,
|
|
202
|
+
channel: ChannelProtocol,
|
|
203
|
+
parameter: str,
|
|
204
|
+
parameter_data: ParameterData,
|
|
205
|
+
) -> type[GenericDataPointAny] | None:
|
|
206
|
+
"""
|
|
207
|
+
Determine the appropriate data point type for a parameter.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
channel: The channel the data point belongs to.
|
|
211
|
+
parameter: The parameter name.
|
|
212
|
+
parameter_data: The parameter description from the backend.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
The data point class to use, or None if no match.
|
|
216
|
+
|
|
217
|
+
"""
|
|
218
|
+
p_type = parameter_data["TYPE"]
|
|
219
|
+
p_operations = parameter_data["OPERATIONS"]
|
|
220
|
+
|
|
221
|
+
if p_operations & Operations.WRITE:
|
|
222
|
+
return cls._resolve_writable(
|
|
223
|
+
channel=channel,
|
|
224
|
+
parameter=parameter,
|
|
225
|
+
parameter_data=parameter_data,
|
|
226
|
+
p_type=p_type,
|
|
227
|
+
p_operations=p_operations,
|
|
228
|
+
)
|
|
229
|
+
return cls._resolve_readonly(parameter=parameter, parameter_data=parameter_data)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# data points that should be wrapped in a new data point on a new category.
|
|
233
|
+
_SWITCH_DP_TO_SENSOR: Final[Mapping[str | tuple[str, ...], Parameter]] = {
|
|
234
|
+
("HmIP-eTRV", "HmIP-HEATING"): Parameter.LEVEL,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@inspector(scope=ServiceScope.INTERNAL)
|
|
239
|
+
def create_data_point_and_append_to_channel(
|
|
240
|
+
*,
|
|
241
|
+
channel: ChannelProtocol,
|
|
242
|
+
paramset_key: ParamsetKey,
|
|
243
|
+
parameter: str,
|
|
244
|
+
parameter_data: ParameterData,
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Decides which generic category should be used, and creates the required data points."""
|
|
247
|
+
_LOGGER.debug(
|
|
248
|
+
"CREATE_DATA_POINTS: Creating data_point for %s, %s, %s",
|
|
249
|
+
channel.address,
|
|
250
|
+
parameter,
|
|
251
|
+
channel.device.interface_id,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if (dp_t := _determine_data_point_type(channel=channel, parameter=parameter, parameter_data=parameter_data)) and (
|
|
255
|
+
dp := _safe_create_data_point(
|
|
256
|
+
dp_t=dp_t, channel=channel, paramset_key=paramset_key, parameter=parameter, parameter_data=parameter_data
|
|
257
|
+
)
|
|
258
|
+
):
|
|
259
|
+
_LOGGER.debug(
|
|
260
|
+
"CREATE_DATA_POINT_AND_APPEND_TO_CHANNEL: %s: %s %s",
|
|
261
|
+
dp.category,
|
|
262
|
+
channel.address,
|
|
263
|
+
parameter,
|
|
264
|
+
)
|
|
265
|
+
channel.add_data_point(data_point=dp)
|
|
266
|
+
if _check_switch_to_sensor(data_point=dp):
|
|
267
|
+
dp.force_to_sensor()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _determine_data_point_type(
|
|
271
|
+
*, channel: ChannelProtocol, parameter: str, parameter_data: ParameterData
|
|
272
|
+
) -> type[GenericDataPointAny] | None:
|
|
273
|
+
"""
|
|
274
|
+
Determine the type of data point based on parameter and operations.
|
|
275
|
+
|
|
276
|
+
Delegates to DataPointTypeResolver for extensible type resolution.
|
|
277
|
+
"""
|
|
278
|
+
return DataPointTypeResolver.resolve(
|
|
279
|
+
channel=channel,
|
|
280
|
+
parameter=parameter,
|
|
281
|
+
parameter_data=parameter_data,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _safe_create_data_point(
|
|
286
|
+
*,
|
|
287
|
+
dp_t: type[GenericDataPointAny],
|
|
288
|
+
channel: ChannelProtocol,
|
|
289
|
+
paramset_key: ParamsetKey,
|
|
290
|
+
parameter: str,
|
|
291
|
+
parameter_data: ParameterData,
|
|
292
|
+
) -> GenericDataPointAny:
|
|
293
|
+
"""Safely create a data point and handle exceptions."""
|
|
294
|
+
try:
|
|
295
|
+
return dp_t(
|
|
296
|
+
channel=channel,
|
|
297
|
+
paramset_key=paramset_key,
|
|
298
|
+
parameter=parameter,
|
|
299
|
+
parameter_data=parameter_data,
|
|
300
|
+
)
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
raise AioHomematicException(
|
|
303
|
+
i18n.tr(
|
|
304
|
+
key="exception.model.generic.create_data_point.failed",
|
|
305
|
+
reason=hms.extract_exc_args(exc=exc),
|
|
306
|
+
)
|
|
307
|
+
) from exc
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _check_switch_to_sensor(*, data_point: GenericDataPointProtocolAny) -> bool:
|
|
311
|
+
"""Check if parameter of a device should be wrapped to a different category."""
|
|
312
|
+
if data_point.device.parameter_visibility_provider.parameter_is_un_ignored(
|
|
313
|
+
channel=data_point.channel,
|
|
314
|
+
paramset_key=data_point.paramset_key,
|
|
315
|
+
parameter=data_point.parameter,
|
|
316
|
+
):
|
|
317
|
+
return False
|
|
318
|
+
for devices, parameter in _SWITCH_DP_TO_SENSOR.items():
|
|
319
|
+
if (
|
|
320
|
+
hms.element_matches_key(
|
|
321
|
+
search_elements=devices,
|
|
322
|
+
compare_with=data_point.device.model,
|
|
323
|
+
)
|
|
324
|
+
and data_point.parameter == parameter
|
|
325
|
+
):
|
|
326
|
+
return True
|
|
327
|
+
return False
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Generic action data points for triggering operations.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
|
|
8
|
+
Actions are used to send data for write only parameters to backend.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from aiohomematic.const import DataPointCategory
|
|
16
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
17
|
+
from aiohomematic.model.support import get_index_of_value_from_value_list
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DpAction(GenericDataPoint[None, Any]):
|
|
21
|
+
"""
|
|
22
|
+
Implementation of an action.
|
|
23
|
+
|
|
24
|
+
This is an internal default category that gets automatically generated.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
__slots__ = ()
|
|
28
|
+
|
|
29
|
+
_category = DataPointCategory.ACTION
|
|
30
|
+
_validate_state_change = False
|
|
31
|
+
|
|
32
|
+
def _prepare_value_for_sending(self, *, value: Any, do_validate: bool = True) -> Any:
|
|
33
|
+
"""Prepare value before sending."""
|
|
34
|
+
# For string-based ENUMs (HmIP), send the string value directly.
|
|
35
|
+
# For index-based ENUMs (HM), convert string to index.
|
|
36
|
+
if self._values is not None and isinstance(value, str) and value in self._values:
|
|
37
|
+
if self._enum_value_is_index:
|
|
38
|
+
return get_index_of_value_from_value_list(value=value, value_list=self._values)
|
|
39
|
+
return value
|
|
40
|
+
return value
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Generic action select data points for write-only parameters with selectable values.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
|
|
8
|
+
Action selects are used for write-only ENUM parameters that have a VALUE_LIST.
|
|
9
|
+
They provide a value getter for displaying the current selection.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from aiohomematic import i18n
|
|
15
|
+
from aiohomematic.const import DataPointCategory
|
|
16
|
+
from aiohomematic.exceptions import ValidationException
|
|
17
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
18
|
+
from aiohomematic.model.support import get_value_from_value_list
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DpActionSelect(GenericDataPoint[int | str | None, int | str]):
|
|
22
|
+
"""
|
|
23
|
+
Implementation of an action with selectable values.
|
|
24
|
+
|
|
25
|
+
This is a write-only data point with a VALUE_LIST that provides
|
|
26
|
+
a value getter for displaying the current selection.
|
|
27
|
+
Used for ENUM parameters that are write-only but have defined values.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
__slots__ = ()
|
|
31
|
+
|
|
32
|
+
_category = DataPointCategory.ACTION_SELECT
|
|
33
|
+
_validate_state_change = False
|
|
34
|
+
|
|
35
|
+
def _get_value(self) -> int | str | None:
|
|
36
|
+
"""Return the value for readings."""
|
|
37
|
+
# For index-based ENUMs (HM), convert integer index to string value.
|
|
38
|
+
if (value := get_value_from_value_list(value=self._value, value_list=self.values)) is not None:
|
|
39
|
+
return value
|
|
40
|
+
# For string-based ENUMs (HmIP), return the string value directly if valid.
|
|
41
|
+
if isinstance(self._value, str) and self._values is not None and self._value in self._values:
|
|
42
|
+
return self._value
|
|
43
|
+
return self._default
|
|
44
|
+
|
|
45
|
+
def _prepare_value_for_sending(self, *, value: int | str, do_validate: bool = True) -> int | str:
|
|
46
|
+
"""Prepare value before sending with validation against value_list."""
|
|
47
|
+
# We allow setting the value via index as well, just in case.
|
|
48
|
+
if isinstance(value, int | float) and self._values and 0 <= value < len(self._values):
|
|
49
|
+
return int(value)
|
|
50
|
+
if self._values and value in self._values:
|
|
51
|
+
# For string-based ENUMs (HmIP), send the string value directly.
|
|
52
|
+
# For index-based ENUMs (HM), convert string to index.
|
|
53
|
+
if self._enum_value_is_index:
|
|
54
|
+
return self._values.index(value)
|
|
55
|
+
return str(value)
|
|
56
|
+
raise ValidationException(
|
|
57
|
+
i18n.tr(
|
|
58
|
+
key="exception.model.action_select.value_not_in_value_list",
|
|
59
|
+
name=self.name,
|
|
60
|
+
unique_id=self.unique_id,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Generic binary sensor data points for boolean state values.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from aiohomematic.const import DataPointCategory
|
|
12
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DpBinarySensor(GenericDataPoint[bool | None, bool]):
|
|
16
|
+
"""
|
|
17
|
+
Implementation of a binary_sensor.
|
|
18
|
+
|
|
19
|
+
This is a default data point that gets automatically generated.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__slots__ = ()
|
|
23
|
+
|
|
24
|
+
_category = DataPointCategory.BINARY_SENSOR
|
|
25
|
+
|
|
26
|
+
def _get_value(self) -> bool | None:
|
|
27
|
+
"""Return the value for readings."""
|
|
28
|
+
if self._value is not None:
|
|
29
|
+
return self._value
|
|
30
|
+
return self._default
|