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,276 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """Module for calculating the apparent temperature in the sensor category."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import Final
9
+
10
+ from aiohomematic.const import CalculatedParameter, DataPointCategory, Parameter, ParameterType, ParamsetKey
11
+ from aiohomematic.interfaces import ChannelProtocol
12
+ from aiohomematic.model.calculated.data_point import CalculatedDataPoint
13
+ from aiohomematic.model.calculated.field import CalculatedDataPointField
14
+ from aiohomematic.model.calculated.support import (
15
+ calculate_apparent_temperature,
16
+ calculate_dew_point,
17
+ calculate_dew_point_spread,
18
+ calculate_enthalpy,
19
+ calculate_frost_point,
20
+ calculate_vapor_concentration,
21
+ )
22
+ from aiohomematic.model.generic import DpSensor
23
+ from aiohomematic.property_decorators import state_property
24
+ from aiohomematic.support import element_matches_key
25
+
26
+ _LOGGER: Final = logging.getLogger(__name__)
27
+
28
+
29
+ class BaseClimateSensor[SensorT: float | None](CalculatedDataPoint[SensorT]):
30
+ """Implementation of a calculated climate sensor."""
31
+
32
+ __slots__ = ()
33
+
34
+ _category = DataPointCategory.SENSOR
35
+
36
+ _dp_humidity: Final = CalculatedDataPointField(
37
+ parameter=Parameter.HUMIDITY,
38
+ paramset_key=ParamsetKey.VALUES,
39
+ dpt=DpSensor,
40
+ fallback_parameters=[Parameter.ACTUAL_HUMIDITY],
41
+ )
42
+ _dp_temperature: Final = CalculatedDataPointField(
43
+ parameter=Parameter.TEMPERATURE,
44
+ paramset_key=ParamsetKey.VALUES,
45
+ dpt=DpSensor,
46
+ fallback_parameters=[Parameter.ACTUAL_TEMPERATURE],
47
+ )
48
+
49
+ def __init__(self, *, channel: ChannelProtocol) -> None:
50
+ """Initialize the data point."""
51
+ super().__init__(channel=channel)
52
+ self._type = ParameterType.FLOAT
53
+
54
+
55
+ class ApparentTemperature(BaseClimateSensor[float | None]):
56
+ """Implementation of a calculated sensor for apparent temperature."""
57
+
58
+ __slots__ = ()
59
+
60
+ _calculated_parameter = CalculatedParameter.APPARENT_TEMPERATURE
61
+
62
+ _dp_wind_speed: Final = CalculatedDataPointField(
63
+ parameter=Parameter.WIND_SPEED,
64
+ paramset_key=ParamsetKey.VALUES,
65
+ dpt=DpSensor,
66
+ )
67
+
68
+ def __init__(self, *, channel: ChannelProtocol) -> None:
69
+ """Initialize the data point."""
70
+ super().__init__(channel=channel)
71
+ self._unit = "°C"
72
+
73
+ @staticmethod
74
+ def is_relevant_for_model(*, channel: ChannelProtocol) -> bool:
75
+ """Return if this calculated data point is relevant for the model."""
76
+ return (
77
+ element_matches_key(
78
+ search_elements=_RELEVANT_MODELS_APPARENT_TEMPERATURE, compare_with=channel.device.model
79
+ )
80
+ and channel.get_generic_data_point(parameter=Parameter.ACTUAL_TEMPERATURE, paramset_key=ParamsetKey.VALUES)
81
+ is not None
82
+ and channel.get_generic_data_point(parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES)
83
+ is not None
84
+ and channel.get_generic_data_point(parameter=Parameter.WIND_SPEED, paramset_key=ParamsetKey.VALUES)
85
+ is not None
86
+ )
87
+
88
+ @state_property
89
+ def value(self) -> float | None:
90
+ """Return the value."""
91
+ if (
92
+ self._dp_temperature.value is not None
93
+ and self._dp_humidity.value is not None
94
+ and self._dp_wind_speed.value is not None
95
+ ):
96
+ return calculate_apparent_temperature(
97
+ temperature=self._dp_temperature.value,
98
+ humidity=self._dp_humidity.value,
99
+ wind_speed=self._dp_wind_speed.value,
100
+ )
101
+ return None
102
+
103
+
104
+ class DewPoint(BaseClimateSensor[float | None]):
105
+ """Implementation of a calculated sensor for dew point."""
106
+
107
+ __slots__ = ()
108
+
109
+ _calculated_parameter = CalculatedParameter.DEW_POINT
110
+
111
+ def __init__(self, *, channel: ChannelProtocol) -> None:
112
+ """Initialize the data point."""
113
+ super().__init__(channel=channel)
114
+ self._unit = "°C"
115
+
116
+ @staticmethod
117
+ def is_relevant_for_model(*, channel: ChannelProtocol) -> bool:
118
+ """Return if this calculated data point is relevant for the model."""
119
+ return _is_relevant_for_model_temperature_and_humidity(channel=channel, relevant_models=None)
120
+
121
+ @state_property
122
+ def value(self) -> float | None:
123
+ """Return the value."""
124
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
125
+ return calculate_dew_point(
126
+ temperature=self._dp_temperature.value,
127
+ humidity=self._dp_humidity.value,
128
+ )
129
+ return None
130
+
131
+
132
+ class DewPointSpread(BaseClimateSensor[float | None]):
133
+ """Implementation of a calculated sensor for dew point spread."""
134
+
135
+ __slots__ = ()
136
+
137
+ _calculated_parameter = CalculatedParameter.DEW_POINT_SPREAD
138
+
139
+ def __init__(self, *, channel: ChannelProtocol) -> None:
140
+ """Initialize the data point."""
141
+ super().__init__(channel=channel)
142
+ self._unit = "K"
143
+
144
+ @staticmethod
145
+ def is_relevant_for_model(*, channel: ChannelProtocol) -> bool:
146
+ """Return if this calculated data point is relevant for the model."""
147
+ return _is_relevant_for_model_temperature_and_humidity(channel=channel, relevant_models=None)
148
+
149
+ @state_property
150
+ def value(self) -> float | None:
151
+ """Return the value."""
152
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
153
+ return calculate_dew_point_spread(
154
+ temperature=self._dp_temperature.value,
155
+ humidity=self._dp_humidity.value,
156
+ )
157
+ return None
158
+
159
+
160
+ class Enthalpy(BaseClimateSensor[float | None]):
161
+ """Implementation of a calculated sensor for enthalpy."""
162
+
163
+ __slots__ = ()
164
+
165
+ _calculated_parameter = CalculatedParameter.ENTHALPY
166
+
167
+ def __init__(self, *, channel: ChannelProtocol) -> None:
168
+ """Initialize the data point."""
169
+ super().__init__(channel=channel)
170
+ self._unit = "kJ/kg"
171
+
172
+ @staticmethod
173
+ def is_relevant_for_model(*, channel: ChannelProtocol) -> bool:
174
+ """Return if this calculated data point is relevant for the model."""
175
+ return _is_relevant_for_model_temperature_and_humidity(channel=channel, relevant_models=None)
176
+
177
+ @state_property
178
+ def value(self) -> float | None:
179
+ """Return the value."""
180
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
181
+ return calculate_enthalpy(
182
+ temperature=self._dp_temperature.value,
183
+ humidity=self._dp_humidity.value,
184
+ )
185
+ return None
186
+
187
+
188
+ class FrostPoint(BaseClimateSensor[float | None]):
189
+ """Implementation of a calculated sensor for frost point."""
190
+
191
+ __slots__ = ()
192
+
193
+ _calculated_parameter = CalculatedParameter.FROST_POINT
194
+
195
+ def __init__(self, *, channel: ChannelProtocol) -> None:
196
+ """Initialize the data point."""
197
+ super().__init__(channel=channel)
198
+ self._unit = "°C"
199
+
200
+ @staticmethod
201
+ def is_relevant_for_model(*, channel: ChannelProtocol) -> bool:
202
+ """Return if this calculated data point is relevant for the model."""
203
+ return _is_relevant_for_model_temperature_and_humidity(
204
+ channel=channel, relevant_models=_RELEVANT_MODELS_FROST_POINT
205
+ )
206
+
207
+ @state_property
208
+ def value(self) -> float | None:
209
+ """Return the value."""
210
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
211
+ return calculate_frost_point(
212
+ temperature=self._dp_temperature.value,
213
+ humidity=self._dp_humidity.value,
214
+ )
215
+ return None
216
+
217
+
218
+ class VaporConcentration(BaseClimateSensor[float | None]):
219
+ """Implementation of a calculated sensor for vapor concentration."""
220
+
221
+ __slots__ = ()
222
+
223
+ _calculated_parameter = CalculatedParameter.VAPOR_CONCENTRATION
224
+
225
+ def __init__(self, *, channel: ChannelProtocol) -> None:
226
+ """Initialize the data point."""
227
+ super().__init__(channel=channel)
228
+ self._unit = "g/m³"
229
+
230
+ @staticmethod
231
+ def is_relevant_for_model(*, channel: ChannelProtocol) -> bool:
232
+ """Return if this calculated data point is relevant for the model."""
233
+ return _is_relevant_for_model_temperature_and_humidity(channel=channel, relevant_models=None)
234
+
235
+ @state_property
236
+ def value(self) -> float | None:
237
+ """Return the value."""
238
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
239
+ return calculate_vapor_concentration(
240
+ temperature=self._dp_temperature.value,
241
+ humidity=self._dp_humidity.value,
242
+ )
243
+ return None
244
+
245
+
246
+ def _is_relevant_for_model_temperature_and_humidity(
247
+ *, channel: ChannelProtocol, relevant_models: tuple[str, ...] | None = None
248
+ ) -> bool:
249
+ """Return if this calculated data point is relevant for the model with temperature and humidity."""
250
+ return (
251
+ (
252
+ relevant_models is not None
253
+ and element_matches_key(search_elements=relevant_models, compare_with=channel.device.model)
254
+ )
255
+ or relevant_models is None
256
+ ) and (
257
+ (
258
+ channel.get_generic_data_point(parameter=Parameter.TEMPERATURE, paramset_key=ParamsetKey.VALUES) is not None
259
+ or channel.get_generic_data_point(parameter=Parameter.ACTUAL_TEMPERATURE, paramset_key=ParamsetKey.VALUES)
260
+ is not None
261
+ )
262
+ and (
263
+ channel.get_generic_data_point(parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES) is not None
264
+ or channel.get_generic_data_point(parameter=Parameter.ACTUAL_HUMIDITY, paramset_key=ParamsetKey.VALUES)
265
+ is not None
266
+ )
267
+ )
268
+
269
+
270
+ _RELEVANT_MODELS_APPARENT_TEMPERATURE: Final[tuple[str, ...]] = ("HmIP-SWO",)
271
+
272
+
273
+ _RELEVANT_MODELS_FROST_POINT: Final[tuple[str, ...]] = (
274
+ "HmIP-STHO",
275
+ "HmIP-SWO",
276
+ )
@@ -0,0 +1,315 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Base implementation for calculated data points deriving values from other data points.
5
+
6
+ Public API of this module is defined by __all__.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import contextlib
12
+ from datetime import datetime
13
+ import logging
14
+ from typing import Final, Unpack, cast
15
+
16
+ from aiohomematic.const import (
17
+ INIT_DATETIME,
18
+ CalculatedParameter,
19
+ CallSource,
20
+ DataPointKey,
21
+ DataPointUsage,
22
+ Operations,
23
+ ParameterType,
24
+ ParamsetKey,
25
+ )
26
+ from aiohomematic.decorators import inspector
27
+ from aiohomematic.interfaces import CallbackDataPointProtocol, ChannelProtocol, GenericDataPointProtocolAny
28
+ from aiohomematic.model.custom import definition as hmed
29
+ from aiohomematic.model.custom.mixins import StateChangeArgs
30
+ from aiohomematic.model.data_point import BaseDataPoint
31
+ from aiohomematic.model.generic import DpDummy
32
+ from aiohomematic.model.support import (
33
+ DataPointNameData,
34
+ DataPointPathData,
35
+ PathData,
36
+ generate_unique_id,
37
+ get_data_point_name_data,
38
+ )
39
+ from aiohomematic.property_decorators import DelegatedProperty, Kind, hm_property, state_property
40
+ from aiohomematic.type_aliases import ParamType, UnsubscribeCallback
41
+
42
+ _LOGGER: Final = logging.getLogger(__name__)
43
+
44
+ # Key type for calculated data point dictionary
45
+ type _DataPointKey = tuple[str, ParamsetKey | None]
46
+
47
+
48
+ class CalculatedDataPoint[ParameterT: ParamType](BaseDataPoint, CallbackDataPointProtocol):
49
+ """Base class for calculated data point."""
50
+
51
+ __slots__ = (
52
+ "_cached_dpk",
53
+ "_data_points",
54
+ "_default",
55
+ "_max",
56
+ "_min",
57
+ "_multiplier",
58
+ "_operations",
59
+ "_service",
60
+ "_type",
61
+ "_unit",
62
+ "_unsubscribe_callbacks",
63
+ "_values",
64
+ "_visible",
65
+ )
66
+
67
+ _calculated_parameter: CalculatedParameter = None # type: ignore[assignment]
68
+
69
+ def __init__(
70
+ self,
71
+ *,
72
+ channel: ChannelProtocol,
73
+ ) -> None:
74
+ """Initialize the data point."""
75
+ self._unsubscribe_callbacks: list[UnsubscribeCallback] = []
76
+ unique_id = generate_unique_id(
77
+ config_provider=channel.device.config_provider,
78
+ address=channel.address,
79
+ parameter=self._calculated_parameter,
80
+ prefix="calculated",
81
+ )
82
+ super().__init__(
83
+ channel=channel,
84
+ unique_id=unique_id,
85
+ is_in_multiple_channels=hmed.is_multi_channel_device(model=channel.device.model, category=self.category),
86
+ )
87
+ self._data_points: Final[dict[_DataPointKey, GenericDataPointProtocolAny]] = {}
88
+ self._type: ParameterType = None # type: ignore[assignment]
89
+ self._values: tuple[str, ...] | None = None
90
+ self._max: ParameterT = None # type: ignore[assignment]
91
+ self._min: ParameterT = None # type: ignore[assignment]
92
+ self._default: ParameterT = None # type: ignore[assignment]
93
+ self._visible: bool = True
94
+ self._service: bool = False
95
+ self._operations: int = 5
96
+ self._unit: str | None = None
97
+ self._multiplier: float = 1.0
98
+ self._post_init()
99
+
100
+ def __del__(self) -> None:
101
+ """Clean up subscriptions when the object is garbage collected."""
102
+ with contextlib.suppress(Exception):
103
+ self.unsubscribe_from_data_point_updated()
104
+
105
+ @staticmethod
106
+ def is_relevant_for_model(*, channel: ChannelProtocol) -> bool:
107
+ """Return if this calculated data point is relevant for the channel."""
108
+ return False
109
+
110
+ _relevant_data_points: Final = DelegatedProperty[tuple[GenericDataPointProtocolAny, ...]](
111
+ path="_readable_data_points"
112
+ )
113
+ default: Final = DelegatedProperty[ParameterT](path="_default")
114
+ hmtype: Final = DelegatedProperty[ParameterType](path="_type")
115
+ max: Final = DelegatedProperty[ParameterT](path="_max", kind=Kind.CONFIG)
116
+ min: Final = DelegatedProperty[ParameterT](path="_min", kind=Kind.CONFIG)
117
+ multiplier: Final = DelegatedProperty[float](path="_multiplier")
118
+ parameter: Final = DelegatedProperty[str](path="_calculated_parameter")
119
+ service: Final = DelegatedProperty[bool](path="_service")
120
+ unit: Final = DelegatedProperty[str | None](path="_unit", kind=Kind.CONFIG)
121
+ values: Final = DelegatedProperty[tuple[str, ...] | None](path="_values", kind=Kind.CONFIG)
122
+ visible: Final = DelegatedProperty[bool](path="_visible")
123
+
124
+ @property
125
+ def _readable_data_points(self) -> tuple[GenericDataPointProtocolAny, ...]:
126
+ """Returns the list of readable data points."""
127
+ return tuple(dp for dp in self._data_points.values() if dp.is_readable)
128
+
129
+ @property
130
+ def _relevant_values_data_points(self) -> tuple[GenericDataPointProtocolAny, ...]:
131
+ """Returns the list of relevant VALUES data points. To be overridden by subclasses."""
132
+ return tuple(dp for dp in self._readable_data_points if dp.paramset_key == ParamsetKey.VALUES)
133
+
134
+ @property
135
+ def _should_publish_data_point_updated_callback(self) -> bool:
136
+ """Check if a data point has been updated or refreshed."""
137
+ if self.published_event_recently: # pylint: disable=using-constant-test
138
+ return False
139
+
140
+ if (relevant_values_data_point := self._relevant_values_data_points) is not None and len(
141
+ relevant_values_data_point
142
+ ) <= 1:
143
+ return True
144
+
145
+ return all(dp.published_event_recently for dp in relevant_values_data_point)
146
+
147
+ @property
148
+ def data_point_name_postfix(self) -> str:
149
+ """Return the data point name postfix."""
150
+ return ""
151
+
152
+ @property
153
+ def has_data_points(self) -> bool:
154
+ """Return if there are data points."""
155
+ return len(self._data_points) > 0
156
+
157
+ @property
158
+ def has_events(self) -> bool:
159
+ """Return, if data_point is supports events."""
160
+ return bool(self._operations & Operations.EVENT)
161
+
162
+ @property
163
+ def is_readable(self) -> bool:
164
+ """Return, if data_point is readable."""
165
+ return bool(self._operations & Operations.READ)
166
+
167
+ @property
168
+ def is_refreshed(self) -> bool:
169
+ """Return if all relevant data_point have been refreshed (received a value)."""
170
+ return all(dp.is_refreshed for dp in self._relevant_data_points)
171
+
172
+ @property
173
+ def is_status_valid(self) -> bool:
174
+ """Return if all relevant data points have valid status."""
175
+ return all(dp.is_status_valid for dp in self._relevant_data_points)
176
+
177
+ @property
178
+ def is_writable(self) -> bool:
179
+ """Return, if data_point is writable."""
180
+ return bool(self._operations & Operations.WRITE)
181
+
182
+ @property
183
+ def paramset_key(self) -> ParamsetKey:
184
+ """Return paramset_key name."""
185
+ return ParamsetKey.CALCULATED
186
+
187
+ @property
188
+ def state_uncertain(self) -> bool:
189
+ """Return, if the state is uncertain."""
190
+ return any(dp.state_uncertain for dp in self._relevant_data_points)
191
+
192
+ @state_property
193
+ def modified_at(self) -> datetime:
194
+ """Return the latest last update timestamp."""
195
+ modified_at: datetime = INIT_DATETIME
196
+ for dp in self._readable_data_points:
197
+ if (data_point_modified_at := dp.modified_at) and data_point_modified_at > modified_at:
198
+ modified_at = data_point_modified_at
199
+ return modified_at
200
+
201
+ @state_property
202
+ def refreshed_at(self) -> datetime:
203
+ """Return the latest last refresh timestamp."""
204
+ refreshed_at: datetime = INIT_DATETIME
205
+ for dp in self._readable_data_points:
206
+ if (data_point_refreshed_at := dp.refreshed_at) and data_point_refreshed_at > refreshed_at:
207
+ refreshed_at = data_point_refreshed_at
208
+ return refreshed_at
209
+
210
+ @hm_property(cached=True)
211
+ def dpk(self) -> DataPointKey:
212
+ """Return data_point key value."""
213
+ return DataPointKey(
214
+ interface_id=self._device.interface_id,
215
+ channel_address=self._channel.address,
216
+ paramset_key=ParamsetKey.CALCULATED,
217
+ parameter=self._calculated_parameter,
218
+ )
219
+
220
+ def is_state_change(self, **kwargs: Unpack[StateChangeArgs]) -> bool:
221
+ """
222
+ Check if the state changes due to kwargs.
223
+
224
+ If the state is uncertain, the state should also marked as changed.
225
+ """
226
+ if self.state_uncertain:
227
+ return True
228
+ _LOGGER.debug("NO_STATE_CHANGE: %s", self.name)
229
+ return False
230
+
231
+ @inspector(re_raise=False)
232
+ async def load_data_point_value(self, *, call_source: CallSource, direct_call: bool = False) -> None:
233
+ """Initialize the data point values."""
234
+ for dp in self._readable_data_points:
235
+ await dp.load_data_point_value(call_source=call_source, direct_call=direct_call)
236
+ self.publish_data_point_updated_event()
237
+
238
+ def unsubscribe_from_data_point_updated(self) -> None:
239
+ """Unsubscribe from all internal update subscriptions."""
240
+ for unreg in self._unsubscribe_callbacks:
241
+ if unreg is not None:
242
+ unreg()
243
+ self._unsubscribe_callbacks.clear()
244
+
245
+ def _add_data_point[DataPointT: GenericDataPointProtocolAny](
246
+ self, *, parameter: str, paramset_key: ParamsetKey | None, dpt: type[DataPointT]
247
+ ) -> DataPointT:
248
+ """Add a new data point and store it in the dict."""
249
+ key: _DataPointKey = (parameter, paramset_key)
250
+ dp = self._resolve_data_point(parameter=parameter, paramset_key=paramset_key)
251
+ self._data_points[key] = dp
252
+ return cast(dpt, dp) # type: ignore[valid-type]
253
+
254
+ def _add_device_data_point[DataPointT: GenericDataPointProtocolAny](
255
+ self,
256
+ *,
257
+ channel_address: str,
258
+ parameter: str,
259
+ paramset_key: ParamsetKey | None,
260
+ dpt: type[DataPointT],
261
+ ) -> DataPointT:
262
+ """Add a new data point from a different channel and store it in the dict."""
263
+ key: _DataPointKey = (parameter, paramset_key)
264
+ if generic_data_point := self._channel.device.get_generic_data_point(
265
+ channel_address=channel_address, parameter=parameter, paramset_key=paramset_key
266
+ ):
267
+ self._data_points[key] = generic_data_point
268
+ self._unsubscribe_callbacks.append(
269
+ generic_data_point.subscribe_to_internal_data_point_updated(
270
+ handler=self.publish_data_point_updated_event
271
+ )
272
+ )
273
+ return cast(dpt, generic_data_point) # type: ignore[valid-type]
274
+ dummy = DpDummy(channel=self._channel, param_field=parameter)
275
+ self._data_points[key] = dummy
276
+ return cast(dpt, dummy) # type: ignore[valid-type]
277
+
278
+ def _get_data_point_name(self) -> DataPointNameData:
279
+ """Create the name for the data point."""
280
+ return get_data_point_name_data(channel=self._channel, parameter=self._calculated_parameter)
281
+
282
+ def _get_data_point_usage(self) -> DataPointUsage:
283
+ """Generate the usage for the data point."""
284
+ return DataPointUsage.DATA_POINT
285
+
286
+ def _get_path_data(self) -> PathData:
287
+ """Return the path data of the data_point."""
288
+ return DataPointPathData(
289
+ interface=self._device.client.interface,
290
+ address=self._device.address,
291
+ channel_no=self._channel.no,
292
+ kind=self._category,
293
+ )
294
+
295
+ def _get_signature(self) -> str:
296
+ """Return the signature of the data_point."""
297
+ return f"{self._category}/{self._channel.device.model}/{self._calculated_parameter}"
298
+
299
+ def _post_init(self) -> None:
300
+ """Post action after initialisation of the data point fields."""
301
+ _LOGGER.debug(
302
+ "POST_INIT_DATA_POINT_FIELDS: Post action after initialisation of the data point fields for %s",
303
+ self.full_name,
304
+ )
305
+
306
+ def _resolve_data_point(self, *, parameter: str, paramset_key: ParamsetKey | None) -> GenericDataPointProtocolAny:
307
+ """Resolve a data point by parameter and paramset_key, returning DpDummy if not found."""
308
+ if generic_data_point := self._channel.get_generic_data_point(parameter=parameter, paramset_key=paramset_key):
309
+ self._unsubscribe_callbacks.append(
310
+ generic_data_point.subscribe_to_internal_data_point_updated(
311
+ handler=self.publish_data_point_updated_event
312
+ )
313
+ )
314
+ return generic_data_point
315
+ return DpDummy(channel=self._channel, param_field=parameter)