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