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,137 @@
1
+ """
2
+ Data point and event model for AioHomematic.
3
+
4
+ This package wires together the model subpackages that turn device/channel
5
+ parameter descriptions into concrete data points and events:
6
+ - generic: Default data point types (switch, number, sensor, select, etc.).
7
+ - custom: Higher-level composites and device-specific behaviors.
8
+ - calculated: Derived metrics (e.g., dew point, apparent temperature).
9
+ - hub: Program and system variable data points from the backend hub (CCU/Homegear).
10
+
11
+ The create_data_points_and_events entrypoint inspects a device’s paramset
12
+ information, applies visibility rules, creates events where appropriate, and
13
+ instantiates the required data point objects. It is invoked during device
14
+ initialization to populate the runtime model used by the central unit.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from typing import Final
21
+
22
+ from aiohomematic.const import (
23
+ CLICK_EVENTS,
24
+ DEVICE_ERROR_EVENTS,
25
+ IMPULSE_EVENTS,
26
+ Flag,
27
+ Operations,
28
+ Parameter,
29
+ ParameterData,
30
+ ParamsetKey,
31
+ )
32
+ from aiohomematic.decorators import inspector
33
+ from aiohomematic.model import device as hmd
34
+ from aiohomematic.model.calculated import create_calculated_data_points
35
+ from aiohomematic.model.event import create_event_and_append_to_channel
36
+ from aiohomematic.model.generic import create_data_point_and_append_to_channel
37
+
38
+ __all__ = ["create_data_points_and_events"]
39
+
40
+ # Some parameters are marked as INTERNAL in the paramset and not considered by default,
41
+ # but some are required and should be added here.
42
+ _ALLOWED_INTERNAL_PARAMETERS: Final[tuple[Parameter, ...]] = (Parameter.DIRECTION,)
43
+ _LOGGER: Final = logging.getLogger(__name__)
44
+
45
+
46
+ @inspector()
47
+ def create_data_points_and_events(device: hmd.Device) -> None:
48
+ """Create the data points associated to this device."""
49
+ for channel in device.channels.values():
50
+ for paramset_key, paramsset_key_descriptions in channel.paramset_descriptions.items():
51
+ if not device.central.parameter_visibility.is_relevant_paramset(
52
+ channel=channel,
53
+ paramset_key=paramset_key,
54
+ ):
55
+ continue
56
+ for (
57
+ parameter,
58
+ parameter_data,
59
+ ) in paramsset_key_descriptions.items():
60
+ parameter_is_un_ignored = channel.device.central.parameter_visibility.parameter_is_un_ignored(
61
+ channel=channel,
62
+ paramset_key=paramset_key,
63
+ parameter=parameter,
64
+ )
65
+ if channel.device.central.parameter_visibility.should_skip_parameter(
66
+ channel=channel,
67
+ paramset_key=paramset_key,
68
+ parameter=parameter,
69
+ parameter_is_un_ignored=parameter_is_un_ignored,
70
+ ):
71
+ continue
72
+ _process_parameter(
73
+ channel=channel,
74
+ paramset_key=paramset_key,
75
+ parameter=parameter,
76
+ parameter_data=parameter_data,
77
+ parameter_is_un_ignored=parameter_is_un_ignored,
78
+ )
79
+
80
+ create_calculated_data_points(channel=channel)
81
+
82
+
83
+ def _process_parameter(
84
+ channel: hmd.Channel,
85
+ paramset_key: ParamsetKey,
86
+ parameter: str,
87
+ parameter_data: ParameterData,
88
+ parameter_is_un_ignored: bool,
89
+ ) -> None:
90
+ """Process individual parameter to create data points and events."""
91
+
92
+ if paramset_key == ParamsetKey.MASTER and parameter_data["OPERATIONS"] == 0:
93
+ # required to fix hm master paramset operation values
94
+ parameter_data["OPERATIONS"] = 3
95
+
96
+ if _should_create_event(parameter_data=parameter_data, parameter=parameter):
97
+ create_event_and_append_to_channel(
98
+ channel=channel,
99
+ parameter=parameter,
100
+ parameter_data=parameter_data,
101
+ )
102
+ if _should_skip_data_point(
103
+ parameter_data=parameter_data, parameter=parameter, parameter_is_un_ignored=parameter_is_un_ignored
104
+ ):
105
+ _LOGGER.debug(
106
+ "CREATE_DATA_POINTS: Skipping %s (no event or internal)",
107
+ parameter,
108
+ )
109
+ return
110
+ # CLICK_EVENTS are allowed for Buttons
111
+ if parameter not in IMPULSE_EVENTS and (not parameter.startswith(DEVICE_ERROR_EVENTS) or parameter_is_un_ignored):
112
+ create_data_point_and_append_to_channel(
113
+ channel=channel,
114
+ paramset_key=paramset_key,
115
+ parameter=parameter,
116
+ parameter_data=parameter_data,
117
+ )
118
+
119
+
120
+ def _should_create_event(parameter_data: ParameterData, parameter: str) -> bool:
121
+ """Determine if an event should be created for the parameter."""
122
+ return bool(
123
+ parameter_data["OPERATIONS"] & Operations.EVENT
124
+ and (parameter in CLICK_EVENTS or parameter.startswith(DEVICE_ERROR_EVENTS) or parameter in IMPULSE_EVENTS)
125
+ )
126
+
127
+
128
+ def _should_skip_data_point(parameter_data: ParameterData, parameter: str, parameter_is_un_ignored: bool) -> bool:
129
+ """Determine if a data point should be skipped."""
130
+ return bool(
131
+ (not parameter_data["OPERATIONS"] & Operations.EVENT and not parameter_data["OPERATIONS"] & Operations.WRITE)
132
+ or (
133
+ parameter_data["FLAGS"] & Flag.INTERNAL
134
+ and parameter not in _ALLOWED_INTERNAL_PARAMETERS
135
+ and not parameter_is_un_ignored
136
+ )
137
+ )
@@ -0,0 +1,65 @@
1
+ """
2
+ Calculated (derived) data points for AioHomematic.
3
+
4
+ This subpackage provides data points whose values are computed from one or more
5
+ underlying device data points. Typical examples include climate-related metrics
6
+ (such as dew point, apparent temperature, frost point, vapor concentration) and
7
+ battery/voltage related assessments (such as operating voltage level).
8
+
9
+ How it works:
10
+ - Each calculated data point is a lightweight model that subscribes to one or
11
+ more generic data points of a channel and recomputes its value when any of
12
+ the source data points change.
13
+ - Relevance is determined per channel. A calculated data point class exposes an
14
+ "is_relevant_for_model" method that decides if the channel provides the
15
+ necessary inputs.
16
+ - Creation is handled centrally via the factory function below.
17
+
18
+ Factory:
19
+ - create_calculated_data_points(channel): Iterates over the known calculated
20
+ implementations, checks their relevance against the given channel, and, if
21
+ applicable, creates and attaches instances to the channel so they behave like
22
+ normal read-only data points.
23
+
24
+ Modules/classes:
25
+ - ApparentTemperature, DewPoint, FrostPoint, VaporConcentration: Climate-related
26
+ sensors implemented in climate.py using well-known formulas (see
27
+ aiohomematic.model.calculated.support for details and references).
28
+ - OperatingVoltageLevel: Interprets battery/voltage values and exposes a human
29
+ readable operating level classification.
30
+
31
+ These calculated data points complement generic and custom data points by
32
+ exposing useful metrics not directly provided by the device/firmware.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import logging
38
+ from typing import Final
39
+
40
+ from aiohomematic.decorators import inspector
41
+ from aiohomematic.model import device as hmd
42
+ from aiohomematic.model.calculated.climate import ApparentTemperature, DewPoint, FrostPoint, VaporConcentration
43
+ from aiohomematic.model.calculated.data_point import CalculatedDataPoint
44
+ from aiohomematic.model.calculated.operating_voltage_level import OperatingVoltageLevel
45
+
46
+ __all__ = [
47
+ "ApparentTemperature",
48
+ "CalculatedDataPoint",
49
+ "DewPoint",
50
+ "FrostPoint",
51
+ "OperatingVoltageLevel",
52
+ "VaporConcentration",
53
+ "create_calculated_data_points",
54
+ ]
55
+
56
+ _CALCULATED_DATA_POINTS: Final = (ApparentTemperature, DewPoint, FrostPoint, OperatingVoltageLevel, VaporConcentration)
57
+ _LOGGER: Final = logging.getLogger(__name__)
58
+
59
+
60
+ @inspector()
61
+ def create_calculated_data_points(channel: hmd.Channel) -> None:
62
+ """Decides which data point category should be used, and creates the required data points."""
63
+ for cdp in _CALCULATED_DATA_POINTS:
64
+ if cdp.is_relevant_for_model(channel=channel):
65
+ channel.add_data_point(data_point=cdp(channel=channel))
@@ -0,0 +1,230 @@
1
+ """Module for calculating the apparent temperature in the sensor category."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Final
7
+
8
+ from aiohomematic.const import CalulatedParameter, DataPointCategory, Parameter, ParameterType, ParamsetKey
9
+ from aiohomematic.model import device as hmd
10
+ from aiohomematic.model.calculated.data_point import CalculatedDataPoint
11
+ from aiohomematic.model.calculated.support import (
12
+ calculate_apparent_temperature,
13
+ calculate_dew_point,
14
+ calculate_frost_point,
15
+ calculate_vapor_concentration,
16
+ )
17
+ from aiohomematic.model.decorators import state_property
18
+ from aiohomematic.model.generic import DpSensor
19
+ from aiohomematic.support import element_matches_key
20
+
21
+ _LOGGER: Final = logging.getLogger(__name__)
22
+
23
+
24
+ class BaseClimateSensor[SensorT: float | None](CalculatedDataPoint[SensorT]):
25
+ """Implementation of a calculated climate sensor."""
26
+
27
+ __slots__ = (
28
+ "_dp_temperature",
29
+ "_dp_humidity",
30
+ "_dp_wind_speed",
31
+ )
32
+
33
+ _category = DataPointCategory.SENSOR
34
+
35
+ def __init__(self, channel: hmd.Channel) -> None:
36
+ """Initialize the data point."""
37
+
38
+ super().__init__(channel=channel)
39
+ self._type = ParameterType.FLOAT
40
+
41
+ def _init_data_point_fields(self) -> None:
42
+ """Init the data point fields."""
43
+ super()._init_data_point_fields()
44
+ self._dp_temperature: DpSensor = (
45
+ self._add_data_point(
46
+ parameter=Parameter.TEMPERATURE, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
47
+ )
48
+ if self._channel.get_generic_data_point(parameter=Parameter.TEMPERATURE, paramset_key=ParamsetKey.VALUES)
49
+ else self._add_data_point(
50
+ parameter=Parameter.ACTUAL_TEMPERATURE, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
51
+ )
52
+ )
53
+ self._dp_humidity: DpSensor = (
54
+ self._add_data_point(
55
+ parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
56
+ )
57
+ if self._channel.get_generic_data_point(parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES)
58
+ else self._add_data_point(
59
+ parameter=Parameter.ACTUAL_HUMIDITY, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
60
+ )
61
+ )
62
+
63
+
64
+ class ApparentTemperature(BaseClimateSensor):
65
+ """Implementation of a calculated sensor for apparent temperature."""
66
+
67
+ __slots__ = ()
68
+
69
+ _calculated_parameter = CalulatedParameter.APPARENT_TEMPERATURE
70
+
71
+ def __init__(self, channel: hmd.Channel) -> None:
72
+ """Initialize the data point."""
73
+ super().__init__(channel=channel)
74
+ self._unit = "°C"
75
+
76
+ def _init_data_point_fields(self) -> None:
77
+ """Init the data point fields."""
78
+ super()._init_data_point_fields()
79
+ self._dp_wind_speed: DpSensor = self._add_data_point(
80
+ parameter=Parameter.WIND_SPEED, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
81
+ )
82
+
83
+ @staticmethod
84
+ def is_relevant_for_model(channel: hmd.Channel) -> bool:
85
+ """Return if this calculated data point is relevant for the model."""
86
+ return (
87
+ element_matches_key(
88
+ search_elements=_RELEVANT_MODELS_APPARENT_TEMPERATURE, compare_with=channel.device.model
89
+ )
90
+ and channel.get_generic_data_point(parameter=Parameter.ACTUAL_TEMPERATURE, paramset_key=ParamsetKey.VALUES)
91
+ is not None
92
+ and channel.get_generic_data_point(parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES)
93
+ is not None
94
+ and channel.get_generic_data_point(parameter=Parameter.WIND_SPEED, paramset_key=ParamsetKey.VALUES)
95
+ is not None
96
+ )
97
+
98
+ @state_property
99
+ def value(self) -> float | None:
100
+ """Return the value."""
101
+ if (
102
+ self._dp_temperature.value is not None
103
+ and self._dp_humidity.value is not None
104
+ and self._dp_wind_speed.value is not None
105
+ ):
106
+ return calculate_apparent_temperature(
107
+ temperature=self._dp_temperature.value,
108
+ humidity=self._dp_humidity.value,
109
+ wind_speed=self._dp_wind_speed.value,
110
+ )
111
+ return None
112
+
113
+
114
+ class DewPoint(BaseClimateSensor):
115
+ """Implementation of a calculated sensor for dew point."""
116
+
117
+ __slots__ = ()
118
+
119
+ _calculated_parameter = CalulatedParameter.DEW_POINT
120
+
121
+ def __init__(self, channel: hmd.Channel) -> None:
122
+ """Initialize the data point."""
123
+ super().__init__(channel=channel)
124
+ self._unit = "°C"
125
+
126
+ @staticmethod
127
+ def is_relevant_for_model(channel: hmd.Channel) -> bool:
128
+ """Return if this calculated data point is relevant for the model."""
129
+ return _is_relevant_for_model_temperature_and_humidity(channel=channel)
130
+
131
+ @state_property
132
+ def value(self) -> float | None:
133
+ """Return the value."""
134
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
135
+ return calculate_dew_point(
136
+ temperature=self._dp_temperature.value,
137
+ humidity=self._dp_humidity.value,
138
+ )
139
+ return None
140
+
141
+
142
+ class FrostPoint(BaseClimateSensor):
143
+ """Implementation of a calculated sensor for frost point."""
144
+
145
+ __slots__ = ()
146
+
147
+ _calculated_parameter = CalulatedParameter.FROST_POINT
148
+
149
+ def __init__(self, channel: hmd.Channel) -> None:
150
+ """Initialize the data point."""
151
+ super().__init__(channel=channel)
152
+ self._unit = "°C"
153
+
154
+ @staticmethod
155
+ def is_relevant_for_model(channel: hmd.Channel) -> bool:
156
+ """Return if this calculated data point is relevant for the model."""
157
+ return _is_relevant_for_model_temperature_and_humidity(
158
+ channel=channel, relevant_models=_RELEVANT_MODELS_FROST_POINT
159
+ )
160
+
161
+ @state_property
162
+ def value(self) -> float | None:
163
+ """Return the value."""
164
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
165
+ return calculate_frost_point(
166
+ temperature=self._dp_temperature.value,
167
+ humidity=self._dp_humidity.value,
168
+ )
169
+ return None
170
+
171
+
172
+ class VaporConcentration(BaseClimateSensor):
173
+ """Implementation of a calculated sensor for vapor concentration."""
174
+
175
+ __slots__ = ()
176
+
177
+ _calculated_parameter = CalulatedParameter.VAPOR_CONCENTRATION
178
+
179
+ def __init__(self, channel: hmd.Channel) -> None:
180
+ """Initialize the data point."""
181
+ super().__init__(channel=channel)
182
+ self._unit = "g/m³"
183
+
184
+ @staticmethod
185
+ def is_relevant_for_model(channel: hmd.Channel) -> bool:
186
+ """Return if this calculated data point is relevant for the model."""
187
+ return _is_relevant_for_model_temperature_and_humidity(channel=channel)
188
+
189
+ @state_property
190
+ def value(self) -> float | None:
191
+ """Return the value."""
192
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
193
+ return calculate_vapor_concentration(
194
+ temperature=self._dp_temperature.value,
195
+ humidity=self._dp_humidity.value,
196
+ )
197
+ return None
198
+
199
+
200
+ def _is_relevant_for_model_temperature_and_humidity(
201
+ channel: hmd.Channel, relevant_models: tuple[str, ...] | None = None
202
+ ) -> bool:
203
+ """Return if this calculated data point is relevant for the model with temperature and humidity."""
204
+ return (
205
+ (
206
+ relevant_models is not None
207
+ and element_matches_key(search_elements=relevant_models, compare_with=channel.device.model)
208
+ )
209
+ or relevant_models is None
210
+ ) and (
211
+ (
212
+ channel.get_generic_data_point(parameter=Parameter.TEMPERATURE, paramset_key=ParamsetKey.VALUES) is not None
213
+ or channel.get_generic_data_point(parameter=Parameter.ACTUAL_TEMPERATURE, paramset_key=ParamsetKey.VALUES)
214
+ is not None
215
+ )
216
+ and (
217
+ channel.get_generic_data_point(parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES) is not None
218
+ or channel.get_generic_data_point(parameter=Parameter.ACTUAL_HUMIDITY, paramset_key=ParamsetKey.VALUES)
219
+ is not None
220
+ )
221
+ )
222
+
223
+
224
+ _RELEVANT_MODELS_APPARENT_TEMPERATURE: Final[tuple[str, ...]] = ("HmIP-SWO",)
225
+
226
+
227
+ _RELEVANT_MODELS_FROST_POINT: Final[tuple[str, ...]] = (
228
+ "HmIP-STHO",
229
+ "HmIP-SWO",
230
+ )