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.
- aiohomematic/__init__.py +110 -0
- aiohomematic/_log_context_protocol.py +29 -0
- aiohomematic/api.py +410 -0
- aiohomematic/async_support.py +250 -0
- aiohomematic/backend_detection.py +462 -0
- aiohomematic/central/__init__.py +103 -0
- aiohomematic/central/async_rpc_server.py +760 -0
- aiohomematic/central/central_unit.py +1152 -0
- aiohomematic/central/config.py +463 -0
- aiohomematic/central/config_builder.py +772 -0
- aiohomematic/central/connection_state.py +160 -0
- aiohomematic/central/coordinators/__init__.py +38 -0
- aiohomematic/central/coordinators/cache.py +414 -0
- aiohomematic/central/coordinators/client.py +480 -0
- aiohomematic/central/coordinators/connection_recovery.py +1141 -0
- aiohomematic/central/coordinators/device.py +1166 -0
- aiohomematic/central/coordinators/event.py +514 -0
- aiohomematic/central/coordinators/hub.py +532 -0
- aiohomematic/central/decorators.py +184 -0
- aiohomematic/central/device_registry.py +229 -0
- aiohomematic/central/events/__init__.py +104 -0
- aiohomematic/central/events/bus.py +1392 -0
- aiohomematic/central/events/integration.py +424 -0
- aiohomematic/central/events/types.py +194 -0
- aiohomematic/central/health.py +762 -0
- aiohomematic/central/rpc_server.py +353 -0
- aiohomematic/central/scheduler.py +794 -0
- aiohomematic/central/state_machine.py +391 -0
- aiohomematic/client/__init__.py +203 -0
- aiohomematic/client/_rpc_errors.py +187 -0
- aiohomematic/client/backends/__init__.py +48 -0
- aiohomematic/client/backends/base.py +335 -0
- aiohomematic/client/backends/capabilities.py +138 -0
- aiohomematic/client/backends/ccu.py +487 -0
- aiohomematic/client/backends/factory.py +116 -0
- aiohomematic/client/backends/homegear.py +294 -0
- aiohomematic/client/backends/json_ccu.py +252 -0
- aiohomematic/client/backends/protocol.py +316 -0
- aiohomematic/client/ccu.py +1857 -0
- aiohomematic/client/circuit_breaker.py +459 -0
- aiohomematic/client/config.py +64 -0
- aiohomematic/client/handlers/__init__.py +40 -0
- aiohomematic/client/handlers/backup.py +157 -0
- aiohomematic/client/handlers/base.py +79 -0
- aiohomematic/client/handlers/device_ops.py +1085 -0
- aiohomematic/client/handlers/firmware.py +144 -0
- aiohomematic/client/handlers/link_mgmt.py +199 -0
- aiohomematic/client/handlers/metadata.py +436 -0
- aiohomematic/client/handlers/programs.py +144 -0
- aiohomematic/client/handlers/sysvars.py +100 -0
- aiohomematic/client/interface_client.py +1304 -0
- aiohomematic/client/json_rpc.py +2068 -0
- aiohomematic/client/request_coalescer.py +282 -0
- aiohomematic/client/rpc_proxy.py +629 -0
- aiohomematic/client/state_machine.py +324 -0
- aiohomematic/const.py +2207 -0
- aiohomematic/context.py +275 -0
- aiohomematic/converter.py +270 -0
- aiohomematic/decorators.py +390 -0
- aiohomematic/exceptions.py +185 -0
- aiohomematic/hmcli.py +997 -0
- aiohomematic/i18n.py +193 -0
- aiohomematic/interfaces/__init__.py +407 -0
- aiohomematic/interfaces/central.py +1067 -0
- aiohomematic/interfaces/client.py +1096 -0
- aiohomematic/interfaces/coordinators.py +63 -0
- aiohomematic/interfaces/model.py +1921 -0
- aiohomematic/interfaces/operations.py +217 -0
- aiohomematic/logging_context.py +134 -0
- aiohomematic/metrics/__init__.py +125 -0
- aiohomematic/metrics/_protocols.py +140 -0
- aiohomematic/metrics/aggregator.py +534 -0
- aiohomematic/metrics/dataclasses.py +489 -0
- aiohomematic/metrics/emitter.py +292 -0
- aiohomematic/metrics/events.py +183 -0
- aiohomematic/metrics/keys.py +300 -0
- aiohomematic/metrics/observer.py +563 -0
- aiohomematic/metrics/stats.py +172 -0
- aiohomematic/model/__init__.py +189 -0
- aiohomematic/model/availability.py +65 -0
- aiohomematic/model/calculated/__init__.py +89 -0
- aiohomematic/model/calculated/climate.py +276 -0
- aiohomematic/model/calculated/data_point.py +315 -0
- aiohomematic/model/calculated/field.py +147 -0
- aiohomematic/model/calculated/operating_voltage_level.py +286 -0
- aiohomematic/model/calculated/support.py +232 -0
- aiohomematic/model/custom/__init__.py +214 -0
- aiohomematic/model/custom/capabilities/__init__.py +67 -0
- aiohomematic/model/custom/capabilities/climate.py +41 -0
- aiohomematic/model/custom/capabilities/light.py +87 -0
- aiohomematic/model/custom/capabilities/lock.py +44 -0
- aiohomematic/model/custom/capabilities/siren.py +63 -0
- aiohomematic/model/custom/climate.py +1130 -0
- aiohomematic/model/custom/cover.py +722 -0
- aiohomematic/model/custom/data_point.py +360 -0
- aiohomematic/model/custom/definition.py +300 -0
- aiohomematic/model/custom/field.py +89 -0
- aiohomematic/model/custom/light.py +1174 -0
- aiohomematic/model/custom/lock.py +322 -0
- aiohomematic/model/custom/mixins.py +445 -0
- aiohomematic/model/custom/profile.py +945 -0
- aiohomematic/model/custom/registry.py +251 -0
- aiohomematic/model/custom/siren.py +462 -0
- aiohomematic/model/custom/switch.py +195 -0
- aiohomematic/model/custom/text_display.py +289 -0
- aiohomematic/model/custom/valve.py +78 -0
- aiohomematic/model/data_point.py +1416 -0
- aiohomematic/model/device.py +1840 -0
- aiohomematic/model/event.py +216 -0
- aiohomematic/model/generic/__init__.py +327 -0
- aiohomematic/model/generic/action.py +40 -0
- aiohomematic/model/generic/action_select.py +62 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +31 -0
- aiohomematic/model/generic/data_point.py +177 -0
- aiohomematic/model/generic/dummy.py +150 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +56 -0
- aiohomematic/model/generic/sensor.py +76 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +33 -0
- aiohomematic/model/hub/__init__.py +100 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/connectivity.py +190 -0
- aiohomematic/model/hub/data_point.py +342 -0
- aiohomematic/model/hub/hub.py +864 -0
- aiohomematic/model/hub/inbox.py +135 -0
- aiohomematic/model/hub/install_mode.py +393 -0
- aiohomematic/model/hub/metrics.py +208 -0
- aiohomematic/model/hub/number.py +42 -0
- aiohomematic/model/hub/select.py +52 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +43 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/hub/update.py +221 -0
- aiohomematic/model/support.py +592 -0
- aiohomematic/model/update.py +140 -0
- aiohomematic/model/week_profile.py +1827 -0
- aiohomematic/property_decorators.py +719 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
- aiohomematic/rega_scripts/create_backup_start.fn +28 -0
- aiohomematic/rega_scripts/create_backup_status.fn +89 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
- aiohomematic/rega_scripts/get_backend_info.fn +25 -0
- aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_service_messages.fn +83 -0
- aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
- aiohomematic/rega_scripts/set_program_state.fn +17 -0
- aiohomematic/rega_scripts/set_system_variable.fn +19 -0
- aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
- aiohomematic/schemas.py +256 -0
- aiohomematic/store/__init__.py +55 -0
- aiohomematic/store/dynamic/__init__.py +43 -0
- aiohomematic/store/dynamic/command.py +250 -0
- aiohomematic/store/dynamic/data.py +175 -0
- aiohomematic/store/dynamic/details.py +187 -0
- aiohomematic/store/dynamic/ping_pong.py +416 -0
- aiohomematic/store/persistent/__init__.py +71 -0
- aiohomematic/store/persistent/base.py +285 -0
- aiohomematic/store/persistent/device.py +233 -0
- aiohomematic/store/persistent/incident.py +380 -0
- aiohomematic/store/persistent/paramset.py +241 -0
- aiohomematic/store/persistent/session.py +556 -0
- aiohomematic/store/serialization.py +150 -0
- aiohomematic/store/storage.py +689 -0
- aiohomematic/store/types.py +526 -0
- aiohomematic/store/visibility/__init__.py +40 -0
- aiohomematic/store/visibility/parser.py +141 -0
- aiohomematic/store/visibility/registry.py +722 -0
- aiohomematic/store/visibility/rules.py +307 -0
- aiohomematic/strings.json +237 -0
- aiohomematic/support.py +706 -0
- aiohomematic/tracing.py +236 -0
- aiohomematic/translations/de.json +237 -0
- aiohomematic/translations/en.json +237 -0
- aiohomematic/type_aliases.py +51 -0
- aiohomematic/validator.py +128 -0
- aiohomematic-2026.1.29.dist-info/METADATA +296 -0
- aiohomematic-2026.1.29.dist-info/RECORD +188 -0
- aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
- aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
- aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
- 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)
|