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.
Files changed (188) hide show
  1. aiohomematic/__init__.py +110 -0
  2. aiohomematic/_log_context_protocol.py +29 -0
  3. aiohomematic/api.py +410 -0
  4. aiohomematic/async_support.py +250 -0
  5. aiohomematic/backend_detection.py +462 -0
  6. aiohomematic/central/__init__.py +103 -0
  7. aiohomematic/central/async_rpc_server.py +760 -0
  8. aiohomematic/central/central_unit.py +1152 -0
  9. aiohomematic/central/config.py +463 -0
  10. aiohomematic/central/config_builder.py +772 -0
  11. aiohomematic/central/connection_state.py +160 -0
  12. aiohomematic/central/coordinators/__init__.py +38 -0
  13. aiohomematic/central/coordinators/cache.py +414 -0
  14. aiohomematic/central/coordinators/client.py +480 -0
  15. aiohomematic/central/coordinators/connection_recovery.py +1141 -0
  16. aiohomematic/central/coordinators/device.py +1166 -0
  17. aiohomematic/central/coordinators/event.py +514 -0
  18. aiohomematic/central/coordinators/hub.py +532 -0
  19. aiohomematic/central/decorators.py +184 -0
  20. aiohomematic/central/device_registry.py +229 -0
  21. aiohomematic/central/events/__init__.py +104 -0
  22. aiohomematic/central/events/bus.py +1392 -0
  23. aiohomematic/central/events/integration.py +424 -0
  24. aiohomematic/central/events/types.py +194 -0
  25. aiohomematic/central/health.py +762 -0
  26. aiohomematic/central/rpc_server.py +353 -0
  27. aiohomematic/central/scheduler.py +794 -0
  28. aiohomematic/central/state_machine.py +391 -0
  29. aiohomematic/client/__init__.py +203 -0
  30. aiohomematic/client/_rpc_errors.py +187 -0
  31. aiohomematic/client/backends/__init__.py +48 -0
  32. aiohomematic/client/backends/base.py +335 -0
  33. aiohomematic/client/backends/capabilities.py +138 -0
  34. aiohomematic/client/backends/ccu.py +487 -0
  35. aiohomematic/client/backends/factory.py +116 -0
  36. aiohomematic/client/backends/homegear.py +294 -0
  37. aiohomematic/client/backends/json_ccu.py +252 -0
  38. aiohomematic/client/backends/protocol.py +316 -0
  39. aiohomematic/client/ccu.py +1857 -0
  40. aiohomematic/client/circuit_breaker.py +459 -0
  41. aiohomematic/client/config.py +64 -0
  42. aiohomematic/client/handlers/__init__.py +40 -0
  43. aiohomematic/client/handlers/backup.py +157 -0
  44. aiohomematic/client/handlers/base.py +79 -0
  45. aiohomematic/client/handlers/device_ops.py +1085 -0
  46. aiohomematic/client/handlers/firmware.py +144 -0
  47. aiohomematic/client/handlers/link_mgmt.py +199 -0
  48. aiohomematic/client/handlers/metadata.py +436 -0
  49. aiohomematic/client/handlers/programs.py +144 -0
  50. aiohomematic/client/handlers/sysvars.py +100 -0
  51. aiohomematic/client/interface_client.py +1304 -0
  52. aiohomematic/client/json_rpc.py +2068 -0
  53. aiohomematic/client/request_coalescer.py +282 -0
  54. aiohomematic/client/rpc_proxy.py +629 -0
  55. aiohomematic/client/state_machine.py +324 -0
  56. aiohomematic/const.py +2207 -0
  57. aiohomematic/context.py +275 -0
  58. aiohomematic/converter.py +270 -0
  59. aiohomematic/decorators.py +390 -0
  60. aiohomematic/exceptions.py +185 -0
  61. aiohomematic/hmcli.py +997 -0
  62. aiohomematic/i18n.py +193 -0
  63. aiohomematic/interfaces/__init__.py +407 -0
  64. aiohomematic/interfaces/central.py +1067 -0
  65. aiohomematic/interfaces/client.py +1096 -0
  66. aiohomematic/interfaces/coordinators.py +63 -0
  67. aiohomematic/interfaces/model.py +1921 -0
  68. aiohomematic/interfaces/operations.py +217 -0
  69. aiohomematic/logging_context.py +134 -0
  70. aiohomematic/metrics/__init__.py +125 -0
  71. aiohomematic/metrics/_protocols.py +140 -0
  72. aiohomematic/metrics/aggregator.py +534 -0
  73. aiohomematic/metrics/dataclasses.py +489 -0
  74. aiohomematic/metrics/emitter.py +292 -0
  75. aiohomematic/metrics/events.py +183 -0
  76. aiohomematic/metrics/keys.py +300 -0
  77. aiohomematic/metrics/observer.py +563 -0
  78. aiohomematic/metrics/stats.py +172 -0
  79. aiohomematic/model/__init__.py +189 -0
  80. aiohomematic/model/availability.py +65 -0
  81. aiohomematic/model/calculated/__init__.py +89 -0
  82. aiohomematic/model/calculated/climate.py +276 -0
  83. aiohomematic/model/calculated/data_point.py +315 -0
  84. aiohomematic/model/calculated/field.py +147 -0
  85. aiohomematic/model/calculated/operating_voltage_level.py +286 -0
  86. aiohomematic/model/calculated/support.py +232 -0
  87. aiohomematic/model/custom/__init__.py +214 -0
  88. aiohomematic/model/custom/capabilities/__init__.py +67 -0
  89. aiohomematic/model/custom/capabilities/climate.py +41 -0
  90. aiohomematic/model/custom/capabilities/light.py +87 -0
  91. aiohomematic/model/custom/capabilities/lock.py +44 -0
  92. aiohomematic/model/custom/capabilities/siren.py +63 -0
  93. aiohomematic/model/custom/climate.py +1130 -0
  94. aiohomematic/model/custom/cover.py +722 -0
  95. aiohomematic/model/custom/data_point.py +360 -0
  96. aiohomematic/model/custom/definition.py +300 -0
  97. aiohomematic/model/custom/field.py +89 -0
  98. aiohomematic/model/custom/light.py +1174 -0
  99. aiohomematic/model/custom/lock.py +322 -0
  100. aiohomematic/model/custom/mixins.py +445 -0
  101. aiohomematic/model/custom/profile.py +945 -0
  102. aiohomematic/model/custom/registry.py +251 -0
  103. aiohomematic/model/custom/siren.py +462 -0
  104. aiohomematic/model/custom/switch.py +195 -0
  105. aiohomematic/model/custom/text_display.py +289 -0
  106. aiohomematic/model/custom/valve.py +78 -0
  107. aiohomematic/model/data_point.py +1416 -0
  108. aiohomematic/model/device.py +1840 -0
  109. aiohomematic/model/event.py +216 -0
  110. aiohomematic/model/generic/__init__.py +327 -0
  111. aiohomematic/model/generic/action.py +40 -0
  112. aiohomematic/model/generic/action_select.py +62 -0
  113. aiohomematic/model/generic/binary_sensor.py +30 -0
  114. aiohomematic/model/generic/button.py +31 -0
  115. aiohomematic/model/generic/data_point.py +177 -0
  116. aiohomematic/model/generic/dummy.py +150 -0
  117. aiohomematic/model/generic/number.py +76 -0
  118. aiohomematic/model/generic/select.py +56 -0
  119. aiohomematic/model/generic/sensor.py +76 -0
  120. aiohomematic/model/generic/switch.py +54 -0
  121. aiohomematic/model/generic/text.py +33 -0
  122. aiohomematic/model/hub/__init__.py +100 -0
  123. aiohomematic/model/hub/binary_sensor.py +24 -0
  124. aiohomematic/model/hub/button.py +28 -0
  125. aiohomematic/model/hub/connectivity.py +190 -0
  126. aiohomematic/model/hub/data_point.py +342 -0
  127. aiohomematic/model/hub/hub.py +864 -0
  128. aiohomematic/model/hub/inbox.py +135 -0
  129. aiohomematic/model/hub/install_mode.py +393 -0
  130. aiohomematic/model/hub/metrics.py +208 -0
  131. aiohomematic/model/hub/number.py +42 -0
  132. aiohomematic/model/hub/select.py +52 -0
  133. aiohomematic/model/hub/sensor.py +37 -0
  134. aiohomematic/model/hub/switch.py +43 -0
  135. aiohomematic/model/hub/text.py +30 -0
  136. aiohomematic/model/hub/update.py +221 -0
  137. aiohomematic/model/support.py +592 -0
  138. aiohomematic/model/update.py +140 -0
  139. aiohomematic/model/week_profile.py +1827 -0
  140. aiohomematic/property_decorators.py +719 -0
  141. aiohomematic/py.typed +0 -0
  142. aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
  143. aiohomematic/rega_scripts/create_backup_start.fn +28 -0
  144. aiohomematic/rega_scripts/create_backup_status.fn +89 -0
  145. aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
  146. aiohomematic/rega_scripts/get_backend_info.fn +25 -0
  147. aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
  148. aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
  149. aiohomematic/rega_scripts/get_serial.fn +44 -0
  150. aiohomematic/rega_scripts/get_service_messages.fn +83 -0
  151. aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
  152. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
  153. aiohomematic/rega_scripts/set_program_state.fn +17 -0
  154. aiohomematic/rega_scripts/set_system_variable.fn +19 -0
  155. aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
  156. aiohomematic/schemas.py +256 -0
  157. aiohomematic/store/__init__.py +55 -0
  158. aiohomematic/store/dynamic/__init__.py +43 -0
  159. aiohomematic/store/dynamic/command.py +250 -0
  160. aiohomematic/store/dynamic/data.py +175 -0
  161. aiohomematic/store/dynamic/details.py +187 -0
  162. aiohomematic/store/dynamic/ping_pong.py +416 -0
  163. aiohomematic/store/persistent/__init__.py +71 -0
  164. aiohomematic/store/persistent/base.py +285 -0
  165. aiohomematic/store/persistent/device.py +233 -0
  166. aiohomematic/store/persistent/incident.py +380 -0
  167. aiohomematic/store/persistent/paramset.py +241 -0
  168. aiohomematic/store/persistent/session.py +556 -0
  169. aiohomematic/store/serialization.py +150 -0
  170. aiohomematic/store/storage.py +689 -0
  171. aiohomematic/store/types.py +526 -0
  172. aiohomematic/store/visibility/__init__.py +40 -0
  173. aiohomematic/store/visibility/parser.py +141 -0
  174. aiohomematic/store/visibility/registry.py +722 -0
  175. aiohomematic/store/visibility/rules.py +307 -0
  176. aiohomematic/strings.json +237 -0
  177. aiohomematic/support.py +706 -0
  178. aiohomematic/tracing.py +236 -0
  179. aiohomematic/translations/de.json +237 -0
  180. aiohomematic/translations/en.json +237 -0
  181. aiohomematic/type_aliases.py +51 -0
  182. aiohomematic/validator.py +128 -0
  183. aiohomematic-2026.1.29.dist-info/METADATA +296 -0
  184. aiohomematic-2026.1.29.dist-info/RECORD +188 -0
  185. aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
  186. aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
  187. aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
  188. 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