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,234 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025
3
+ """
4
+ A number of functions used to calculate values based on existing data.
5
+
6
+ Climate related formula are based on:
7
+ - thermal comfort (https://github.com/dolezsa/thermal_comfort) ground works.
8
+ - https://gist.github.com/E3V3A/8f9f0aa18380d4ab2546cd50b725a219
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import math
15
+ from typing import Final
16
+
17
+ from aiohomematic.support import extract_exc_args
18
+
19
+ _DEFAULT_PRESSURE_HPA: Final = 1013.25
20
+ _LOGGER: Final = logging.getLogger(__name__)
21
+
22
+
23
+ def calculate_dew_point_spread(*, temperature: float, humidity: int) -> float | None:
24
+ """
25
+ Calculate the dew point spread.
26
+
27
+ Dew point spread = Difference between current air temperature and dew point.
28
+ Specifies the safety margin against condensation(K).
29
+ """
30
+ if dew_point := calculate_dew_point(temperature=temperature, humidity=humidity):
31
+ return round(temperature - dew_point, 2)
32
+ return None
33
+
34
+
35
+ def calculate_enthalpy(
36
+ *, temperature: float, humidity: int, pressure_hPa: float = _DEFAULT_PRESSURE_HPA
37
+ ) -> float | None:
38
+ """
39
+ Calculate the enthalpy based on temperature and humidity.
40
+
41
+ Calculates the specific enthalpy of humid air in kJ/kg (relative to dry air).
42
+ temperature: Air temperature in °C
43
+ humidity: Relative humidity in %
44
+ pressure_hPa: Air pressure (default: 1013.25 hPa)
45
+
46
+ """
47
+
48
+ # Saturation vapor pressure according to Magnus in hPa
49
+ e_s = 6.112 * math.exp((17.62 * temperature) / (243.12 + temperature))
50
+ e = humidity / 100.0 * e_s # aktueller Dampfdruck in hPa
51
+
52
+ # Mixing ratio (g water / kg dry air)
53
+ r = 622 * e / (pressure_hPa - e)
54
+
55
+ # Specific enthalpy (kJ/kg dry air)
56
+ h = 1.006 * temperature + r * (2501 + 1.86 * temperature) / 1000 # in kJ/kg
57
+ return round(h, 2)
58
+
59
+
60
+ def _calculate_heat_index(*, temperature: float, humidity: int) -> float:
61
+ """
62
+ Calculate the Heat Index (feels like temperature) based on the NOAA equation.
63
+
64
+ References:
65
+ [1] https://en.wikipedia.org/wiki/Heat_index
66
+ [2] http://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml
67
+ [3] https://github.com/geanders/weathermetrics/blob/master/R/heat_index.R
68
+ [4] https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3801457/
69
+
70
+ """
71
+
72
+ # SI units (Celsius)
73
+ c1 = -8.78469475556
74
+ c2 = 1.61139411
75
+ c3 = 2.33854883889
76
+ c4 = -0.14611605
77
+ c5 = -0.012308094
78
+ c6 = -0.0164248277778
79
+ c7 = 0.002211732
80
+ c8 = 0.00072546
81
+ c9 = -0.000003582
82
+
83
+ temperature_fahrenheit = (temperature * 9 / 5) + 32
84
+ heat_index_fahrenheit = 0.5 * (
85
+ temperature_fahrenheit + 61.0 + (temperature_fahrenheit - 68.0) * 1.2 + humidity * 0.094
86
+ )
87
+
88
+ if ((heat_index_fahrenheit + temperature_fahrenheit) / 2) >= 80: # [°F]
89
+ # temperature > 27C and humidity > 40 %
90
+ heat_index_celsius = math.fsum(
91
+ [
92
+ c1,
93
+ c2 * temperature,
94
+ c3 * humidity,
95
+ c4 * temperature * humidity,
96
+ c5 * temperature**2,
97
+ c6 * humidity**2,
98
+ c7 * temperature**2 * humidity,
99
+ c8 * temperature * humidity**2,
100
+ c9 * temperature**2 * humidity**2,
101
+ ]
102
+ )
103
+ else:
104
+ heat_index_celsius = (heat_index_fahrenheit - 32) * 5 / 9
105
+
106
+ return heat_index_celsius
107
+
108
+
109
+ def _calculate_wind_chill(*, temperature: float, wind_speed: float) -> float | None:
110
+ """
111
+ Calculate the Wind Chill (feels like temperature) based on NOAA.
112
+
113
+ References:
114
+ [1] https://en.wikipedia.org/wiki/Wind_chill
115
+ [2] https://www.wpc.ncep.noaa.gov/html/windchill.shtml
116
+
117
+ """
118
+ # Wind Chill Temperature is only defined for temperatures at or below 10°C and wind speeds above 4.8 Km/h.
119
+ if temperature > 10 or wind_speed <= 4.8: # if temperature > 50 or wind_speed <= 3: # (°F, Mph)
120
+ return None
121
+
122
+ return float(13.12 + (0.6215 * temperature) - 11.37 * wind_speed**0.16 + 0.3965 * temperature * wind_speed**0.16)
123
+
124
+
125
+ def calculate_vapor_concentration(*, temperature: float, humidity: int) -> float | None:
126
+ """Calculate the vapor concentration."""
127
+ try:
128
+ abs_temperature = temperature + 273.15
129
+ vapor_concentration = 6.112
130
+ vapor_concentration *= math.exp((17.67 * temperature) / (243.5 + temperature))
131
+ vapor_concentration *= humidity
132
+ vapor_concentration *= 2.1674
133
+ vapor_concentration /= abs_temperature
134
+
135
+ return round(vapor_concentration, 2)
136
+ except ValueError as verr:
137
+ _LOGGER.debug(
138
+ "Unable to calculate 'vapor concentration' with temperature: %s, humidity: %s (%s)",
139
+ temperature,
140
+ humidity,
141
+ extract_exc_args(exc=verr),
142
+ )
143
+ return None
144
+
145
+
146
+ def calculate_apparent_temperature(*, temperature: float, humidity: int, wind_speed: float) -> float | None:
147
+ """Calculate the apparent temperature based on NOAA."""
148
+ try:
149
+ if temperature <= 10 and wind_speed > 4.8:
150
+ # Wind Chill for low temp cases (and wind)
151
+ apparent_temperature = _calculate_wind_chill(temperature=temperature, wind_speed=wind_speed)
152
+ elif temperature >= 26.7:
153
+ # Heat Index for High temp cases
154
+ apparent_temperature = _calculate_heat_index(temperature=temperature, humidity=humidity)
155
+ else:
156
+ apparent_temperature = temperature
157
+
158
+ return round(apparent_temperature, 1) # type: ignore[arg-type]
159
+ except ValueError as verr:
160
+ if temperature == 0.0 and humidity == 0:
161
+ return 0.0
162
+ _LOGGER.debug(
163
+ "Unable to calculate 'apparent temperature' with temperature: %s, humidity: %s (%s)",
164
+ temperature,
165
+ humidity,
166
+ extract_exc_args(exc=verr),
167
+ )
168
+ return None
169
+
170
+
171
+ def calculate_dew_point(*, temperature: float, humidity: int) -> float | None:
172
+ """Calculate the dew point."""
173
+ try:
174
+ a0 = 373.15 / (273.15 + temperature)
175
+ s = -7.90298 * (a0 - 1)
176
+ s += 5.02808 * math.log(a0, 10)
177
+ s += -1.3816e-7 * (pow(10, (11.344 * (1 - 1 / a0))) - 1)
178
+ s += 8.1328e-3 * (pow(10, (-3.49149 * (a0 - 1))) - 1)
179
+ s += math.log(1013.246, 10)
180
+ vp = pow(10, s - 3) * humidity
181
+ td = math.log(vp / 0.61078)
182
+
183
+ return round((241.88 * td) / (17.558 - td), 1)
184
+ except ValueError as verr:
185
+ if temperature == 0.0 and humidity == 0:
186
+ return 0.0
187
+ _LOGGER.debug(
188
+ "Unable to calculate 'dew point' with temperature: %s, humidity: %s (%s)",
189
+ temperature,
190
+ humidity,
191
+ extract_exc_args(exc=verr),
192
+ )
193
+ return None
194
+
195
+
196
+ def calculate_frost_point(*, temperature: float, humidity: int) -> float | None:
197
+ """Calculate the frost point."""
198
+ try:
199
+ if (dew_point := calculate_dew_point(temperature=temperature, humidity=humidity)) is None:
200
+ return None
201
+ t = temperature + 273.15
202
+ td = dew_point + 273.15
203
+
204
+ return round((td + (2671.02 / ((2954.61 / t) + 2.193665 * math.log(t) - 13.3448)) - t) - 273.15, 1)
205
+ except ValueError as verr:
206
+ if temperature == 0.0 and humidity == 0:
207
+ return 0.0
208
+ _LOGGER.debug(
209
+ "Unable to calculate 'frost point' with temperature: %s, humidity: %s (%s)",
210
+ temperature,
211
+ humidity,
212
+ extract_exc_args(exc=verr),
213
+ )
214
+ return None
215
+
216
+
217
+ def calculate_operating_voltage_level(
218
+ *, operating_voltage: float | None, low_bat_limit: float | None, voltage_max: float | None
219
+ ) -> float | None:
220
+ """Return the operating voltage level."""
221
+ if operating_voltage is None or low_bat_limit is None or voltage_max is None:
222
+ return None
223
+ return max(
224
+ 0,
225
+ min(
226
+ 100,
227
+ float(
228
+ round(
229
+ ((float(operating_voltage) - low_bat_limit) / (voltage_max - low_bat_limit) * 100),
230
+ 1,
231
+ )
232
+ ),
233
+ ),
234
+ )
@@ -0,0 +1,177 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025
3
+ """
4
+ Custom data points for AioHomematic.
5
+
6
+ This subpackage provides higher-level, device-specific data points that combine
7
+ multiple backend parameters into single, meaningful entities (for example: a
8
+ thermostat, a blind with tilt, a fixed-color light, a lock, a siren, a switch,
9
+ or an irrigation valve). It also contains discovery helpers and a schema-based
10
+ validation for model-specific configurations.
11
+
12
+ What this package does
13
+ - Discovery: create_custom_data_points() inspects a device model and, if a
14
+ matching custom definition exists and the device is not ignored for customs,
15
+ creates the appropriate custom data point(s) and attaches them to the device.
16
+ - Definitions: The definition module holds the catalog of supported models and
17
+ the rules that describe which parameters form each custom entity. It exposes
18
+ helpers to query availability, enumerate required parameters, and validate the
19
+ definition schema.
20
+ - Specializations: Rich custom data point classes for climate, light, cover,
21
+ lock, siren, switch, and irrigation valve provide tailored behavior and an API
22
+ focused on user intent (e.g., set_temperature, open_tilt, set_profile,
23
+ turn_on with effect, lock/open, vent, etc.).
24
+
25
+ How it relates to the generic layer
26
+ Custom data points build on top of generic data points. While the generic layer
27
+ maps one backend parameter to one data point, this package groups multiple
28
+ parameters across channels (where needed) into a single higher-level entity. The
29
+ result is a simpler interface for automations and UIs, while still allowing the
30
+ underlying generic data points to be created when desired.
31
+
32
+ Public API entry points commonly used by integrators
33
+ - create_custom_data_points(device): Run discovery and attach custom data points.
34
+ - data_point_definition_exists(model): Check if a custom definition is available.
35
+ - get_custom_configs(model, category=None): Retrieve the CustomConfig entries
36
+ used to create custom data points for a model (optionally filtered by
37
+ category).
38
+ - get_required_parameters(): Return all parameters that must be fetched to allow
39
+ custom data points to function properly.
40
+ - validate_custom_data_point_definition(): Validate the internal definition
41
+ schema; useful in tests and development.
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import logging
47
+ from typing import Final
48
+
49
+ from aiohomematic.decorators import inspector
50
+ from aiohomematic.model import device as hmd
51
+ from aiohomematic.model.custom.climate import (
52
+ PROFILE_DICT,
53
+ PROFILE_PREFIX,
54
+ SIMPLE_PROFILE_DICT,
55
+ SIMPLE_WEEKDAY_LIST,
56
+ WEEKDAY_DICT,
57
+ BaseCustomDpClimate,
58
+ ClimateActivity,
59
+ ClimateMode,
60
+ ClimateProfile,
61
+ CustomDpIpThermostat,
62
+ CustomDpRfThermostat,
63
+ CustomDpSimpleRfThermostat,
64
+ ScheduleProfile,
65
+ ScheduleWeekday,
66
+ )
67
+ from aiohomematic.model.custom.cover import (
68
+ CustomDpBlind,
69
+ CustomDpCover,
70
+ CustomDpGarage,
71
+ CustomDpIpBlind,
72
+ CustomDpWindowDrive,
73
+ )
74
+ from aiohomematic.model.custom.data_point import CustomDataPoint
75
+ from aiohomematic.model.custom.definition import (
76
+ data_point_definition_exists,
77
+ get_custom_configs,
78
+ get_required_parameters,
79
+ validate_custom_data_point_definition,
80
+ )
81
+ from aiohomematic.model.custom.light import (
82
+ CustomDpColorDimmer,
83
+ CustomDpColorDimmerEffect,
84
+ CustomDpColorTempDimmer,
85
+ CustomDpDimmer,
86
+ CustomDpIpDrgDaliLight,
87
+ CustomDpIpFixedColorLight,
88
+ CustomDpIpRGBWLight,
89
+ LightOffArgs,
90
+ LightOnArgs,
91
+ )
92
+ from aiohomematic.model.custom.lock import (
93
+ BaseCustomDpLock,
94
+ CustomDpButtonLock,
95
+ CustomDpIpLock,
96
+ CustomDpRfLock,
97
+ LockState,
98
+ )
99
+ from aiohomematic.model.custom.siren import BaseCustomDpSiren, CustomDpIpSiren, CustomDpIpSirenSmoke, SirenOnArgs
100
+ from aiohomematic.model.custom.switch import CustomDpSwitch
101
+ from aiohomematic.model.custom.valve import CustomDpIpIrrigationValve
102
+
103
+ __all__ = [
104
+ "BaseCustomDpClimate",
105
+ "BaseCustomDpLock",
106
+ "BaseCustomDpSiren",
107
+ "ClimateActivity",
108
+ "ClimateMode",
109
+ "ClimateProfile",
110
+ "CustomDataPoint",
111
+ "CustomDpBlind",
112
+ "CustomDpButtonLock",
113
+ "CustomDpColorDimmer",
114
+ "CustomDpColorDimmerEffect",
115
+ "CustomDpColorTempDimmer",
116
+ "CustomDpCover",
117
+ "CustomDpDimmer",
118
+ "CustomDpGarage",
119
+ "CustomDpIpBlind",
120
+ "CustomDpIpDrgDaliLight",
121
+ "CustomDpIpFixedColorLight",
122
+ "CustomDpIpIrrigationValve",
123
+ "CustomDpIpLock",
124
+ "CustomDpIpRGBWLight",
125
+ "CustomDpIpSiren",
126
+ "CustomDpIpSirenSmoke",
127
+ "CustomDpIpThermostat",
128
+ "CustomDpRfLock",
129
+ "CustomDpRfThermostat",
130
+ "CustomDpSimpleRfThermostat",
131
+ "CustomDpSwitch",
132
+ "CustomDpWindowDrive",
133
+ "LightOffArgs",
134
+ "LightOnArgs",
135
+ "LockState",
136
+ "PROFILE_DICT",
137
+ "PROFILE_PREFIX",
138
+ "SIMPLE_PROFILE_DICT",
139
+ "SIMPLE_WEEKDAY_LIST",
140
+ "ScheduleProfile",
141
+ "ScheduleWeekday",
142
+ "SirenOnArgs",
143
+ "WEEKDAY_DICT",
144
+ "create_custom_data_points",
145
+ "data_point_definition_exists",
146
+ "get_custom_configs",
147
+ "get_required_parameters",
148
+ "validate_custom_data_point_definition",
149
+ ]
150
+
151
+ _LOGGER: Final = logging.getLogger(__name__)
152
+
153
+
154
+ @inspector
155
+ def create_custom_data_points(*, device: hmd.Device) -> None:
156
+ """Decides which data point category should be used, and creates the required data points."""
157
+
158
+ if device.ignore_for_custom_data_point:
159
+ _LOGGER.debug(
160
+ "CREATE_CUSTOM_DATA_POINTS: Ignoring for custom data point: %s, %s, %s due to ignored",
161
+ device.interface_id,
162
+ device,
163
+ device.model,
164
+ )
165
+ return
166
+ if data_point_definition_exists(model=device.model):
167
+ _LOGGER.debug(
168
+ "CREATE_CUSTOM_DATA_POINTS: Handling custom data point integration: %s, %s, %s",
169
+ device.interface_id,
170
+ device,
171
+ device.model,
172
+ )
173
+
174
+ # Call the custom creation function.
175
+ for custom_config in get_custom_configs(model=device.model):
176
+ for channel in device.channels.values():
177
+ custom_config.make_ce_func(channel=channel, custom_config=custom_config)