aiohomematic 2025.11.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aiohomematic might be problematic. Click here for more details.

Files changed (77) hide show
  1. aiohomematic/__init__.py +61 -0
  2. aiohomematic/async_support.py +212 -0
  3. aiohomematic/central/__init__.py +2309 -0
  4. aiohomematic/central/decorators.py +155 -0
  5. aiohomematic/central/rpc_server.py +295 -0
  6. aiohomematic/client/__init__.py +1848 -0
  7. aiohomematic/client/_rpc_errors.py +81 -0
  8. aiohomematic/client/json_rpc.py +1326 -0
  9. aiohomematic/client/rpc_proxy.py +311 -0
  10. aiohomematic/const.py +1127 -0
  11. aiohomematic/context.py +18 -0
  12. aiohomematic/converter.py +108 -0
  13. aiohomematic/decorators.py +302 -0
  14. aiohomematic/exceptions.py +164 -0
  15. aiohomematic/hmcli.py +186 -0
  16. aiohomematic/model/__init__.py +140 -0
  17. aiohomematic/model/calculated/__init__.py +84 -0
  18. aiohomematic/model/calculated/climate.py +290 -0
  19. aiohomematic/model/calculated/data_point.py +327 -0
  20. aiohomematic/model/calculated/operating_voltage_level.py +299 -0
  21. aiohomematic/model/calculated/support.py +234 -0
  22. aiohomematic/model/custom/__init__.py +177 -0
  23. aiohomematic/model/custom/climate.py +1532 -0
  24. aiohomematic/model/custom/cover.py +792 -0
  25. aiohomematic/model/custom/data_point.py +334 -0
  26. aiohomematic/model/custom/definition.py +871 -0
  27. aiohomematic/model/custom/light.py +1128 -0
  28. aiohomematic/model/custom/lock.py +394 -0
  29. aiohomematic/model/custom/siren.py +275 -0
  30. aiohomematic/model/custom/support.py +41 -0
  31. aiohomematic/model/custom/switch.py +175 -0
  32. aiohomematic/model/custom/valve.py +114 -0
  33. aiohomematic/model/data_point.py +1123 -0
  34. aiohomematic/model/device.py +1445 -0
  35. aiohomematic/model/event.py +208 -0
  36. aiohomematic/model/generic/__init__.py +217 -0
  37. aiohomematic/model/generic/action.py +34 -0
  38. aiohomematic/model/generic/binary_sensor.py +30 -0
  39. aiohomematic/model/generic/button.py +27 -0
  40. aiohomematic/model/generic/data_point.py +171 -0
  41. aiohomematic/model/generic/dummy.py +147 -0
  42. aiohomematic/model/generic/number.py +76 -0
  43. aiohomematic/model/generic/select.py +39 -0
  44. aiohomematic/model/generic/sensor.py +74 -0
  45. aiohomematic/model/generic/switch.py +54 -0
  46. aiohomematic/model/generic/text.py +29 -0
  47. aiohomematic/model/hub/__init__.py +333 -0
  48. aiohomematic/model/hub/binary_sensor.py +24 -0
  49. aiohomematic/model/hub/button.py +28 -0
  50. aiohomematic/model/hub/data_point.py +340 -0
  51. aiohomematic/model/hub/number.py +39 -0
  52. aiohomematic/model/hub/select.py +49 -0
  53. aiohomematic/model/hub/sensor.py +37 -0
  54. aiohomematic/model/hub/switch.py +44 -0
  55. aiohomematic/model/hub/text.py +30 -0
  56. aiohomematic/model/support.py +586 -0
  57. aiohomematic/model/update.py +143 -0
  58. aiohomematic/property_decorators.py +496 -0
  59. aiohomematic/py.typed +0 -0
  60. aiohomematic/rega_scripts/fetch_all_device_data.fn +92 -0
  61. aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
  62. aiohomematic/rega_scripts/get_serial.fn +44 -0
  63. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
  64. aiohomematic/rega_scripts/set_program_state.fn +12 -0
  65. aiohomematic/rega_scripts/set_system_variable.fn +15 -0
  66. aiohomematic/store/__init__.py +34 -0
  67. aiohomematic/store/dynamic.py +551 -0
  68. aiohomematic/store/persistent.py +988 -0
  69. aiohomematic/store/visibility.py +812 -0
  70. aiohomematic/support.py +664 -0
  71. aiohomematic/validator.py +112 -0
  72. aiohomematic-2025.11.3.dist-info/METADATA +144 -0
  73. aiohomematic-2025.11.3.dist-info/RECORD +77 -0
  74. aiohomematic-2025.11.3.dist-info/WHEEL +5 -0
  75. aiohomematic-2025.11.3.dist-info/entry_points.txt +2 -0
  76. aiohomematic-2025.11.3.dist-info/licenses/LICENSE +21 -0
  77. aiohomematic-2025.11.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,334 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025
3
+ """Module with base class for custom data points."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Callable, Mapping
8
+ from datetime import datetime
9
+ import logging
10
+ from typing import Any, Final, cast
11
+
12
+ from aiohomematic.const import (
13
+ CALLBACK_TYPE,
14
+ CDPD,
15
+ INIT_DATETIME,
16
+ CallSource,
17
+ DataPointKey,
18
+ DataPointUsage,
19
+ DeviceProfile,
20
+ Field,
21
+ )
22
+ from aiohomematic.model import device as hmd
23
+ from aiohomematic.model.custom import definition as hmed
24
+ from aiohomematic.model.custom.support import CustomConfig
25
+ from aiohomematic.model.data_point import BaseDataPoint
26
+ from aiohomematic.model.generic import DpDummy, data_point as hmge
27
+ from aiohomematic.model.support import (
28
+ DataPointNameData,
29
+ DataPointPathData,
30
+ PathData,
31
+ check_channel_is_the_only_primary_channel,
32
+ get_custom_data_point_name,
33
+ )
34
+ from aiohomematic.property_decorators import state_property
35
+ from aiohomematic.support import get_channel_address
36
+
37
+ _LOGGER: Final = logging.getLogger(__name__)
38
+
39
+
40
+ class CustomDataPoint(BaseDataPoint):
41
+ """Base class for custom data point."""
42
+
43
+ __slots__ = (
44
+ "_allow_undefined_generic_data_points",
45
+ "_custom_config",
46
+ "_custom_data_point_def",
47
+ "_data_points",
48
+ "_device_def",
49
+ "_device_profile",
50
+ "_extended",
51
+ "_group_no",
52
+ "_unregister_callbacks",
53
+ )
54
+
55
+ def __init__(
56
+ self,
57
+ *,
58
+ channel: hmd.Channel,
59
+ unique_id: str,
60
+ device_profile: DeviceProfile,
61
+ device_def: Mapping[str, Any],
62
+ custom_data_point_def: Mapping[int | tuple[int, ...], tuple[str, ...]],
63
+ group_no: int,
64
+ custom_config: CustomConfig,
65
+ ) -> None:
66
+ """Initialize the data point."""
67
+ self._unregister_callbacks: list[CALLBACK_TYPE] = []
68
+ self._device_profile: Final = device_profile
69
+ # required for name in BaseDataPoint
70
+ self._device_def: Final = device_def
71
+ self._custom_data_point_def: Final = custom_data_point_def
72
+ self._group_no: int = group_no
73
+ self._custom_config: Final = custom_config
74
+ self._extended: Final = custom_config.extended
75
+ super().__init__(
76
+ channel=channel,
77
+ unique_id=unique_id,
78
+ is_in_multiple_channels=hmed.is_multi_channel_device(model=channel.device.model, category=self.category),
79
+ )
80
+ self._allow_undefined_generic_data_points: Final[bool] = self._device_def[CDPD.ALLOW_UNDEFINED_GENERIC_DPS]
81
+ self._data_points: Final[dict[Field, hmge.GenericDataPoint]] = {}
82
+ self._init_data_points()
83
+ self._init_data_point_fields()
84
+
85
+ def _init_data_point_fields(self) -> None:
86
+ """Init the data point fields."""
87
+ _LOGGER.debug(
88
+ "INIT_DATA_POINT_FIELDS: Initialising the data point fields for %s",
89
+ self.full_name,
90
+ )
91
+
92
+ @property
93
+ def allow_undefined_generic_data_points(self) -> bool:
94
+ """Return if undefined generic data points of this device are allowed."""
95
+ return self._allow_undefined_generic_data_points
96
+
97
+ @property
98
+ def group_no(self) -> int | None:
99
+ """Return the base channel no of the data point."""
100
+ return self._group_no
101
+
102
+ @state_property
103
+ def modified_at(self) -> datetime:
104
+ """Return the latest last update timestamp."""
105
+ modified_at: datetime = INIT_DATETIME
106
+ for dp in self._readable_data_points:
107
+ if (data_point_modified_at := dp.modified_at) and data_point_modified_at > modified_at:
108
+ modified_at = data_point_modified_at
109
+ return modified_at
110
+
111
+ @state_property
112
+ def refreshed_at(self) -> datetime:
113
+ """Return the latest last refresh timestamp."""
114
+ refreshed_at: datetime = INIT_DATETIME
115
+ for dp in self._readable_data_points:
116
+ if (data_point_refreshed_at := dp.refreshed_at) and data_point_refreshed_at > refreshed_at:
117
+ refreshed_at = data_point_refreshed_at
118
+ return refreshed_at
119
+
120
+ @property
121
+ def unconfirmed_last_values_send(self) -> Mapping[Field, Any]:
122
+ """Return the unconfirmed values send for the data point."""
123
+ unconfirmed_values: dict[Field, Any] = {}
124
+ for field, dp in self._data_points.items():
125
+ if (unconfirmed_value := dp.unconfirmed_last_value_send) is not None:
126
+ unconfirmed_values[field] = unconfirmed_value
127
+ return unconfirmed_values
128
+
129
+ @property
130
+ def has_data_points(self) -> bool:
131
+ """Return if there are data points."""
132
+ return len(self._data_points) > 0
133
+
134
+ @property
135
+ def is_valid(self) -> bool:
136
+ """Return if the state is valid."""
137
+ return all(dp.is_valid for dp in self._relevant_data_points)
138
+
139
+ @property
140
+ def state_uncertain(self) -> bool:
141
+ """Return, if the state is uncertain."""
142
+ return any(dp.state_uncertain for dp in self._relevant_data_points)
143
+
144
+ @property
145
+ def _readable_data_points(self) -> tuple[hmge.GenericDataPoint, ...]:
146
+ """Returns the list of readable data points."""
147
+ return tuple(dp for dp in self._data_points.values() if dp.is_readable)
148
+
149
+ @property
150
+ def _relevant_data_points(self) -> tuple[hmge.GenericDataPoint, ...]:
151
+ """Returns the list of relevant data points. To be overridden by subclasses."""
152
+ return self._readable_data_points
153
+
154
+ @property
155
+ def data_point_name_postfix(self) -> str:
156
+ """Return the data point name postfix."""
157
+ return ""
158
+
159
+ def _get_path_data(self) -> PathData:
160
+ """Return the path data of the data_point."""
161
+ return DataPointPathData(
162
+ interface=self._device.client.interface,
163
+ address=self._device.address,
164
+ channel_no=self._channel.no,
165
+ kind=self._category,
166
+ )
167
+
168
+ def _get_data_point_name(self) -> DataPointNameData:
169
+ """Create the name for the data point."""
170
+ is_only_primary_channel = check_channel_is_the_only_primary_channel(
171
+ current_channel_no=self._channel.no,
172
+ device_def=self._device_def,
173
+ device_has_multiple_channels=self.is_in_multiple_channels,
174
+ )
175
+ return get_custom_data_point_name(
176
+ channel=self._channel,
177
+ is_only_primary_channel=is_only_primary_channel,
178
+ ignore_multiple_channels_for_name=self._ignore_multiple_channels_for_name,
179
+ usage=self._get_data_point_usage(),
180
+ postfix=self.data_point_name_postfix.replace("_", " ").title(),
181
+ )
182
+
183
+ def _get_data_point_usage(self) -> DataPointUsage:
184
+ """Generate the usage for the data point."""
185
+ if self._forced_usage:
186
+ return self._forced_usage
187
+ if self._channel.no in self._custom_config.channels:
188
+ return DataPointUsage.CDP_PRIMARY
189
+ return DataPointUsage.CDP_SECONDARY
190
+
191
+ def _get_signature(self) -> str:
192
+ """Return the signature of the data_point."""
193
+ return f"{self._category}/{self._channel.device.model}/{self.data_point_name_postfix}"
194
+
195
+ async def load_data_point_value(self, *, call_source: CallSource, direct_call: bool = False) -> None:
196
+ """Init the data point values."""
197
+ for dp in self._readable_data_points:
198
+ await dp.load_data_point_value(call_source=call_source, direct_call=direct_call)
199
+ self.emit_data_point_updated_event()
200
+
201
+ def is_state_change(self, **kwargs: Any) -> bool:
202
+ """
203
+ Check if the state changes due to kwargs.
204
+
205
+ If the state is uncertain, the state should also marked as changed.
206
+ """
207
+ if self.state_uncertain:
208
+ return True
209
+ _LOGGER.debug("NO_STATE_CHANGE: %s", self.name)
210
+ return False
211
+
212
+ def _init_data_points(self) -> None:
213
+ """Init data point collection."""
214
+ # Add repeating fields
215
+ for field_name, parameter in self._device_def.get(CDPD.REPEATABLE_FIELDS, {}).items():
216
+ if dp := self._device.get_generic_data_point(channel_address=self._channel.address, parameter=parameter):
217
+ self._add_data_point(field=field_name, data_point=dp, is_visible=False)
218
+
219
+ # Add visible repeating fields
220
+ for field_name, parameter in self._device_def.get(CDPD.VISIBLE_REPEATABLE_FIELDS, {}).items():
221
+ if dp := self._device.get_generic_data_point(channel_address=self._channel.address, parameter=parameter):
222
+ self._add_data_point(field=field_name, data_point=dp, is_visible=True)
223
+
224
+ if self._extended:
225
+ if fixed_channels := self._extended.fixed_channels:
226
+ for channel_no, mapping in fixed_channels.items():
227
+ for field_name, parameter in mapping.items():
228
+ channel_address = get_channel_address(
229
+ device_address=self._device.address, channel_no=channel_no
230
+ )
231
+ if dp := self._device.get_generic_data_point(
232
+ channel_address=channel_address, parameter=parameter
233
+ ):
234
+ self._add_data_point(field=field_name, data_point=dp)
235
+ if additional_dps := self._extended.additional_data_points:
236
+ self._mark_data_points(custom_data_point_def=additional_dps)
237
+
238
+ # Add device fields
239
+ self._add_data_points(
240
+ field_dict_name=CDPD.FIELDS,
241
+ )
242
+ # Add visible device fields
243
+ self._add_data_points(
244
+ field_dict_name=CDPD.VISIBLE_FIELDS,
245
+ is_visible=True,
246
+ )
247
+
248
+ # Add default device data points
249
+ self._mark_data_points(custom_data_point_def=self._custom_data_point_def)
250
+ # add default data points
251
+ if hmed.get_include_default_data_points(device_profile=self._device_profile):
252
+ self._mark_data_points(custom_data_point_def=hmed.get_default_data_points())
253
+
254
+ def _add_data_points(self, *, field_dict_name: CDPD, is_visible: bool | None = None) -> None:
255
+ """Add data points to custom data point."""
256
+ fields = self._device_def.get(field_dict_name, {})
257
+ for channel_no, channel in fields.items():
258
+ for field, parameter in channel.items():
259
+ channel_address = get_channel_address(device_address=self._device.address, channel_no=channel_no)
260
+ if dp := self._device.get_generic_data_point(channel_address=channel_address, parameter=parameter):
261
+ self._add_data_point(field=field, data_point=dp, is_visible=is_visible)
262
+
263
+ def _add_data_point(
264
+ self,
265
+ *,
266
+ field: Field,
267
+ data_point: hmge.GenericDataPoint | None,
268
+ is_visible: bool | None = None,
269
+ ) -> None:
270
+ """Add data point to collection and register callback."""
271
+ if not data_point:
272
+ return
273
+ if is_visible is True and data_point.is_forced_sensor is False:
274
+ data_point.force_usage(forced_usage=DataPointUsage.CDP_VISIBLE)
275
+ elif is_visible is False and data_point.is_forced_sensor is False:
276
+ data_point.force_usage(forced_usage=DataPointUsage.NO_CREATE)
277
+
278
+ self._unregister_callbacks.append(
279
+ data_point.register_internal_data_point_updated_callback(cb=self.emit_data_point_updated_event)
280
+ )
281
+ self._data_points[field] = data_point
282
+
283
+ def _unregister_data_point_updated_callback(self, *, cb: Callable, custom_id: str) -> None:
284
+ """Unregister update callback."""
285
+ for unregister in self._unregister_callbacks:
286
+ if unregister is not None:
287
+ unregister()
288
+
289
+ super()._unregister_data_point_updated_callback(cb=cb, custom_id=custom_id)
290
+
291
+ def _mark_data_points(self, *, custom_data_point_def: Mapping[int | tuple[int, ...], tuple[str, ...]]) -> None:
292
+ """Mark data points to be created, even though a custom data point is present."""
293
+ if not custom_data_point_def:
294
+ return
295
+ for channel_nos, parameters in custom_data_point_def.items():
296
+ if isinstance(channel_nos, int):
297
+ self._mark_data_point(channel_no=channel_nos, parameters=parameters)
298
+ else:
299
+ for channel_no in channel_nos:
300
+ self._mark_data_point(channel_no=channel_no, parameters=parameters)
301
+
302
+ def _mark_data_point(self, *, channel_no: int | None, parameters: tuple[str, ...]) -> None:
303
+ """Mark data point to be created, even though a custom data point is present."""
304
+ channel_address = get_channel_address(device_address=self._device.address, channel_no=channel_no)
305
+
306
+ for parameter in parameters:
307
+ if dp := self._device.get_generic_data_point(channel_address=channel_address, parameter=parameter):
308
+ dp.force_usage(forced_usage=DataPointUsage.DATA_POINT)
309
+
310
+ def _get_data_point[DataPointT: hmge.GenericDataPoint](
311
+ self, *, field: Field, data_point_type: type[DataPointT]
312
+ ) -> DataPointT:
313
+ """Get data point."""
314
+ if dp := self._data_points.get(field):
315
+ if type(dp).__name__ != data_point_type.__name__:
316
+ # not isinstance(data_point, data_point_type): # does not work with generic type
317
+ _LOGGER.debug( # pragma: no cover
318
+ "GET_DATA_POINT: type mismatch for requested sub data_point: "
319
+ "expected: %s, but is %s for field name %s of data_point %s",
320
+ data_point_type.name,
321
+ type(dp),
322
+ field,
323
+ self.name,
324
+ )
325
+ return cast(data_point_type, dp) # type: ignore[valid-type]
326
+ return cast(
327
+ data_point_type, # type:ignore[valid-type]
328
+ DpDummy(channel=self._channel, param_field=field),
329
+ )
330
+
331
+ def has_data_point_key(self, *, data_point_keys: set[DataPointKey]) -> bool:
332
+ """Return if a data_point with one of the data points is part of this data_point."""
333
+ result = [dp for dp in self._data_points.values() if dp.dpk in data_point_keys]
334
+ return len(result) > 0