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
aiohomematic/hmcli.py ADDED
@@ -0,0 +1,186 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025
3
+ #!/usr/bin/python3
4
+ """
5
+ Commandline tool to query Homematic hubs via XML-RPC.
6
+
7
+ Public API of this module is defined by __all__.
8
+
9
+ This module provides a command-line interface; as a library surface it only
10
+ exposes the 'main' entrypoint for invocation. All other names are internal.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from typing import Any
19
+ from xmlrpc.client import ServerProxy
20
+
21
+ from aiohomematic import __version__
22
+ from aiohomematic.const import ParamsetKey
23
+ from aiohomematic.support import build_xml_rpc_headers, build_xml_rpc_uri, get_tls_context
24
+
25
+ # Define public API for this module (CLI only)
26
+ __all__ = ["main"]
27
+
28
+
29
+ def main() -> None:
30
+ """Start the cli."""
31
+ parser = argparse.ArgumentParser(
32
+ description="Commandline tool to query Homematic hubs via XML-RPC",
33
+ )
34
+ parser.add_argument("--version", action="version", version=__version__)
35
+ parser.add_argument(
36
+ "--host",
37
+ "-H",
38
+ required=True,
39
+ type=str,
40
+ help="Hostname / IP address to connect to",
41
+ )
42
+ parser.add_argument(
43
+ "--port",
44
+ "-p",
45
+ required=True,
46
+ type=int,
47
+ help="Port to connect to",
48
+ )
49
+ parser.add_argument(
50
+ "--path",
51
+ type=str,
52
+ help="Path, used for heating groups",
53
+ )
54
+ parser.add_argument(
55
+ "--username",
56
+ "-U",
57
+ nargs="?",
58
+ help="Username required for access",
59
+ )
60
+ parser.add_argument(
61
+ "--password",
62
+ "-P",
63
+ nargs="?",
64
+ help="Password required for access",
65
+ )
66
+ parser.add_argument(
67
+ "--tls",
68
+ "-t",
69
+ action="store_true",
70
+ help="Enable TLS encryption",
71
+ )
72
+ parser.add_argument(
73
+ "--verify",
74
+ "-v",
75
+ action="store_true",
76
+ help="Verify TLS encryption",
77
+ )
78
+ parser.add_argument(
79
+ "--json",
80
+ "-j",
81
+ action="store_true",
82
+ help="Output as JSON",
83
+ )
84
+ parser.add_argument(
85
+ "--address",
86
+ "-a",
87
+ required=True,
88
+ type=str,
89
+ help="Address of Homematic device, including channel",
90
+ )
91
+ parser.add_argument(
92
+ "--paramset_key",
93
+ default=ParamsetKey.VALUES,
94
+ choices=[ParamsetKey.VALUES, ParamsetKey.MASTER],
95
+ help="Paramset of Homematic device. Default: VALUES",
96
+ )
97
+ parser.add_argument(
98
+ "--parameter",
99
+ required=True,
100
+ help="Parameter of Homematic device",
101
+ )
102
+ parser.add_argument(
103
+ "--value",
104
+ type=str,
105
+ help="Value to set for parameter. Use 0/1 for boolean",
106
+ )
107
+ parser.add_argument(
108
+ "--type",
109
+ choices=["int", "float", "bool"],
110
+ help="Type of value when setting a value. Using str if not provided",
111
+ )
112
+ args = parser.parse_args()
113
+
114
+ url = build_xml_rpc_uri(
115
+ host=args.host,
116
+ port=args.port,
117
+ path=args.path,
118
+ tls=args.tls,
119
+ )
120
+ headers = build_xml_rpc_headers(username=args.username, password=args.password)
121
+ context = None
122
+ if args.tls:
123
+ context = get_tls_context(verify_tls=args.verify)
124
+ proxy = ServerProxy(url, context=context, headers=headers)
125
+
126
+ try:
127
+ if args.paramset_key == ParamsetKey.VALUES and args.value is None:
128
+ result = proxy.getValue(args.address, args.parameter)
129
+ if args.json:
130
+ print(
131
+ json.dumps(
132
+ {"address": args.address, "parameter": args.parameter, "value": result}, ensure_ascii=False
133
+ )
134
+ )
135
+ else:
136
+ print(result)
137
+ sys.exit(0)
138
+ elif args.paramset_key == ParamsetKey.VALUES and args.value:
139
+ value: Any
140
+ if args.type == "int":
141
+ value = int(args.value)
142
+ elif args.type == "float":
143
+ value = float(args.value)
144
+ elif args.type == "bool":
145
+ value = bool(int(args.value))
146
+ else:
147
+ value = args.value
148
+ proxy.setValue(args.address, args.parameter, value)
149
+ sys.exit(0)
150
+ elif args.paramset_key == ParamsetKey.MASTER and args.value is None:
151
+ paramset: dict[str, Any] | None
152
+ if (paramset := proxy.getParamset(args.address, args.paramset_key)) and (args.parameter in paramset): # type: ignore[assignment]
153
+ result = paramset[args.parameter]
154
+ if args.json:
155
+ print(
156
+ json.dumps(
157
+ {
158
+ "address": args.address,
159
+ "paramset_key": args.paramset_key,
160
+ "parameter": args.parameter,
161
+ "value": result,
162
+ },
163
+ ensure_ascii=False,
164
+ )
165
+ )
166
+ else:
167
+ print(result)
168
+ sys.exit(0)
169
+ elif args.paramset_key == ParamsetKey.MASTER and args.value:
170
+ if args.type == "int":
171
+ value = int(args.value)
172
+ elif args.type == "float":
173
+ value = float(args.value)
174
+ elif args.type == "bool":
175
+ value = bool(int(args.value))
176
+ else:
177
+ value = args.value
178
+ proxy.putParamset(args.address, args.paramset_key, {args.parameter: value})
179
+ sys.exit(0)
180
+ except Exception as ex:
181
+ print(str(ex), file=sys.stderr)
182
+ sys.exit(1)
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
@@ -0,0 +1,140 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025
3
+ """
4
+ Data point and event model for AioHomematic.
5
+
6
+ This package wires together the model subpackages that turn device/channel
7
+ parameter descriptions into concrete data points and events:
8
+ - generic: Default data point types (switch, number, sensor, select, etc.).
9
+ - custom: Higher-level composites and device-specific behaviors.
10
+ - calculated: Derived metrics (e.g., dew point, apparent temperature).
11
+ - hub: Program and system variable data points from the backend hub.
12
+
13
+ The create_data_points_and_events entrypoint inspects a device’s paramset
14
+ information, applies visibility rules, creates events where appropriate, and
15
+ instantiates the required data point objects. It is invoked during device
16
+ initialization to populate the runtime model used by the central unit.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ from typing import Final
23
+
24
+ from aiohomematic.const import (
25
+ CLICK_EVENTS,
26
+ DEVICE_ERROR_EVENTS,
27
+ IMPULSE_EVENTS,
28
+ Flag,
29
+ Operations,
30
+ Parameter,
31
+ ParameterData,
32
+ ParamsetKey,
33
+ )
34
+ from aiohomematic.decorators import inspector
35
+ from aiohomematic.model import device as hmd
36
+ from aiohomematic.model.calculated import create_calculated_data_points
37
+ from aiohomematic.model.event import create_event_and_append_to_channel
38
+ from aiohomematic.model.generic import create_data_point_and_append_to_channel
39
+
40
+ __all__ = ["create_data_points_and_events"]
41
+
42
+ # Some parameters are marked as INTERNAL in the paramset and not considered by default,
43
+ # but some are required and should be added here.
44
+ _ALLOWED_INTERNAL_PARAMETERS: Final[tuple[Parameter, ...]] = (Parameter.DIRECTION,)
45
+ _LOGGER: Final = logging.getLogger(__name__)
46
+
47
+
48
+ @inspector
49
+ def create_data_points_and_events(*, device: hmd.Device) -> None:
50
+ """Create the data points associated to this device."""
51
+ for channel in device.channels.values():
52
+ for paramset_key, paramsset_key_descriptions in channel.paramset_descriptions.items():
53
+ if not device.central.parameter_visibility.is_relevant_paramset(
54
+ channel=channel,
55
+ paramset_key=paramset_key,
56
+ ):
57
+ continue
58
+ for (
59
+ parameter,
60
+ parameter_data,
61
+ ) in paramsset_key_descriptions.items():
62
+ parameter_is_un_ignored = channel.device.central.parameter_visibility.parameter_is_un_ignored(
63
+ channel=channel,
64
+ paramset_key=paramset_key,
65
+ parameter=parameter,
66
+ )
67
+ if channel.device.central.parameter_visibility.should_skip_parameter(
68
+ channel=channel,
69
+ paramset_key=paramset_key,
70
+ parameter=parameter,
71
+ parameter_is_un_ignored=parameter_is_un_ignored,
72
+ ):
73
+ continue
74
+ _process_parameter(
75
+ channel=channel,
76
+ paramset_key=paramset_key,
77
+ parameter=parameter,
78
+ parameter_data=parameter_data,
79
+ parameter_is_un_ignored=parameter_is_un_ignored,
80
+ )
81
+
82
+ create_calculated_data_points(channel=channel)
83
+
84
+
85
+ def _process_parameter(
86
+ *,
87
+ channel: hmd.Channel,
88
+ paramset_key: ParamsetKey,
89
+ parameter: str,
90
+ parameter_data: ParameterData,
91
+ parameter_is_un_ignored: bool,
92
+ ) -> None:
93
+ """Process individual parameter to create data points and events."""
94
+
95
+ if paramset_key == ParamsetKey.MASTER and parameter_data["OPERATIONS"] == 0:
96
+ # required to fix hm master paramset operation values
97
+ parameter_data["OPERATIONS"] = 3
98
+
99
+ if _should_create_event(parameter_data=parameter_data, parameter=parameter):
100
+ create_event_and_append_to_channel(
101
+ channel=channel,
102
+ parameter=parameter,
103
+ parameter_data=parameter_data,
104
+ )
105
+ if _should_skip_data_point(
106
+ parameter_data=parameter_data, parameter=parameter, parameter_is_un_ignored=parameter_is_un_ignored
107
+ ):
108
+ _LOGGER.debug(
109
+ "CREATE_DATA_POINTS: Skipping %s (no event or internal)",
110
+ parameter,
111
+ )
112
+ return
113
+ # CLICK_EVENTS are allowed for Buttons
114
+ if parameter not in IMPULSE_EVENTS and (not parameter.startswith(DEVICE_ERROR_EVENTS) or parameter_is_un_ignored):
115
+ create_data_point_and_append_to_channel(
116
+ channel=channel,
117
+ paramset_key=paramset_key,
118
+ parameter=parameter,
119
+ parameter_data=parameter_data,
120
+ )
121
+
122
+
123
+ def _should_create_event(*, parameter_data: ParameterData, parameter: str) -> bool:
124
+ """Determine if an event should be created for the parameter."""
125
+ return bool(
126
+ parameter_data["OPERATIONS"] & Operations.EVENT
127
+ and (parameter in CLICK_EVENTS or parameter.startswith(DEVICE_ERROR_EVENTS) or parameter in IMPULSE_EVENTS)
128
+ )
129
+
130
+
131
+ def _should_skip_data_point(*, parameter_data: ParameterData, parameter: str, parameter_is_un_ignored: bool) -> bool:
132
+ """Determine if a data point should be skipped."""
133
+ return bool(
134
+ (not parameter_data["OPERATIONS"] & Operations.EVENT and not parameter_data["OPERATIONS"] & Operations.WRITE)
135
+ or (
136
+ parameter_data["FLAGS"] & Flag.INTERNAL
137
+ and parameter not in _ALLOWED_INTERNAL_PARAMETERS
138
+ and not parameter_is_un_ignored
139
+ )
140
+ )
@@ -0,0 +1,84 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025
3
+ """
4
+ Calculated (derived) data points for AioHomematic.
5
+
6
+ This subpackage provides data points whose values are computed from one or more
7
+ underlying device data points. Typical examples include climate-related metrics
8
+ (such as dew point, apparent temperature, frost point, vapor concentration) and
9
+ battery/voltage related assessments (such as operating voltage level).
10
+
11
+ How it works:
12
+ - Each calculated data point is a lightweight model that subscribes to one or
13
+ more generic data points of a channel and recomputes its value when any of
14
+ the source data points change.
15
+ - Relevance is determined per channel. A calculated data point class exposes an
16
+ "is_relevant_for_model" method that decides if the channel provides the
17
+ necessary inputs.
18
+ - Creation is handled centrally via the factory function below.
19
+
20
+ Factory:
21
+ - create_calculated_data_points(channel): Iterates over the known calculated
22
+ implementations, checks their relevance against the given channel, and, if
23
+ applicable, creates and attaches instances to the channel so they behave like
24
+ normal read-only data points.
25
+
26
+ Modules/classes:
27
+ - ApparentTemperature, DewPoint, DewPointSpread, Enthalphy, FrostPoint, VaporConcentration: Climate-related
28
+ sensors implemented in climate.py using well-known formulas (see
29
+ aiohomematic.model.calculated.support for details and references).
30
+ - OperatingVoltageLevel: Interprets battery/voltage values and exposes a human
31
+ readable operating level classification.
32
+
33
+ These calculated data points complement generic and custom data points by
34
+ exposing useful metrics not directly provided by the device/firmware.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import logging
40
+ from typing import Final
41
+
42
+ from aiohomematic.decorators import inspector
43
+ from aiohomematic.model import device as hmd
44
+ from aiohomematic.model.calculated.climate import (
45
+ ApparentTemperature,
46
+ DewPoint,
47
+ DewPointSpread,
48
+ Enthalpy,
49
+ FrostPoint,
50
+ VaporConcentration,
51
+ )
52
+ from aiohomematic.model.calculated.data_point import CalculatedDataPoint
53
+ from aiohomematic.model.calculated.operating_voltage_level import OperatingVoltageLevel
54
+
55
+ __all__ = [
56
+ "ApparentTemperature",
57
+ "CalculatedDataPoint",
58
+ "DewPoint",
59
+ "DewPointSpread",
60
+ "Enthalpy",
61
+ "FrostPoint",
62
+ "OperatingVoltageLevel",
63
+ "VaporConcentration",
64
+ "create_calculated_data_points",
65
+ ]
66
+
67
+ _CALCULATED_DATA_POINTS: Final = (
68
+ ApparentTemperature,
69
+ DewPoint,
70
+ DewPointSpread,
71
+ Enthalpy,
72
+ FrostPoint,
73
+ OperatingVoltageLevel,
74
+ VaporConcentration,
75
+ )
76
+ _LOGGER: Final = logging.getLogger(__name__)
77
+
78
+
79
+ @inspector
80
+ def create_calculated_data_points(*, channel: hmd.Channel) -> None:
81
+ """Decides which data point category should be used, and creates the required data points."""
82
+ for dp in _CALCULATED_DATA_POINTS:
83
+ if dp.is_relevant_for_model(channel=channel):
84
+ channel.add_data_point(data_point=dp(channel=channel))
@@ -0,0 +1,290 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025
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 CalulatedParameter, DataPointCategory, Parameter, ParameterType, ParamsetKey
11
+ from aiohomematic.model import device as hmd
12
+ from aiohomematic.model.calculated.data_point import CalculatedDataPoint
13
+ from aiohomematic.model.calculated.support import (
14
+ calculate_apparent_temperature,
15
+ calculate_dew_point,
16
+ calculate_dew_point_spread,
17
+ calculate_enthalpy,
18
+ calculate_frost_point,
19
+ calculate_vapor_concentration,
20
+ )
21
+ from aiohomematic.model.generic import DpSensor
22
+ from aiohomematic.property_decorators import state_property
23
+ from aiohomematic.support import element_matches_key
24
+
25
+ _LOGGER: Final = logging.getLogger(__name__)
26
+
27
+
28
+ class BaseClimateSensor[SensorT: float | None](CalculatedDataPoint[SensorT]):
29
+ """Implementation of a calculated climate sensor."""
30
+
31
+ __slots__ = (
32
+ "_dp_temperature",
33
+ "_dp_humidity",
34
+ "_dp_wind_speed",
35
+ )
36
+
37
+ _category = DataPointCategory.SENSOR
38
+
39
+ def __init__(self, *, channel: hmd.Channel) -> None:
40
+ """Initialize the data point."""
41
+
42
+ super().__init__(channel=channel)
43
+ self._type = ParameterType.FLOAT
44
+
45
+ def _init_data_point_fields(self) -> None:
46
+ """Init the data point fields."""
47
+ super()._init_data_point_fields()
48
+ self._dp_temperature: DpSensor = (
49
+ self._add_data_point(
50
+ parameter=Parameter.TEMPERATURE, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
51
+ )
52
+ if self._channel.get_generic_data_point(parameter=Parameter.TEMPERATURE, paramset_key=ParamsetKey.VALUES)
53
+ else self._add_data_point(
54
+ parameter=Parameter.ACTUAL_TEMPERATURE, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
55
+ )
56
+ )
57
+ self._dp_humidity: DpSensor = (
58
+ self._add_data_point(
59
+ parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
60
+ )
61
+ if self._channel.get_generic_data_point(parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES)
62
+ else self._add_data_point(
63
+ parameter=Parameter.ACTUAL_HUMIDITY, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
64
+ )
65
+ )
66
+
67
+
68
+ class ApparentTemperature(BaseClimateSensor):
69
+ """Implementation of a calculated sensor for apparent temperature."""
70
+
71
+ __slots__ = ()
72
+
73
+ _calculated_parameter = CalulatedParameter.APPARENT_TEMPERATURE
74
+
75
+ def __init__(self, *, channel: hmd.Channel) -> None:
76
+ """Initialize the data point."""
77
+ super().__init__(channel=channel)
78
+ self._unit = "°C"
79
+
80
+ def _init_data_point_fields(self) -> None:
81
+ """Init the data point fields."""
82
+ super()._init_data_point_fields()
83
+ self._dp_wind_speed: DpSensor = self._add_data_point(
84
+ parameter=Parameter.WIND_SPEED, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
85
+ )
86
+
87
+ @staticmethod
88
+ def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
89
+ """Return if this calculated data point is relevant for the model."""
90
+ return (
91
+ element_matches_key(
92
+ search_elements=_RELEVANT_MODELS_APPARENT_TEMPERATURE, compare_with=channel.device.model
93
+ )
94
+ and channel.get_generic_data_point(parameter=Parameter.ACTUAL_TEMPERATURE, paramset_key=ParamsetKey.VALUES)
95
+ is not None
96
+ and channel.get_generic_data_point(parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES)
97
+ is not None
98
+ and channel.get_generic_data_point(parameter=Parameter.WIND_SPEED, paramset_key=ParamsetKey.VALUES)
99
+ is not None
100
+ )
101
+
102
+ @state_property
103
+ def value(self) -> float | None:
104
+ """Return the value."""
105
+ if (
106
+ self._dp_temperature.value is not None
107
+ and self._dp_humidity.value is not None
108
+ and self._dp_wind_speed.value is not None
109
+ ):
110
+ return calculate_apparent_temperature(
111
+ temperature=self._dp_temperature.value,
112
+ humidity=self._dp_humidity.value,
113
+ wind_speed=self._dp_wind_speed.value,
114
+ )
115
+ return None
116
+
117
+
118
+ class DewPoint(BaseClimateSensor):
119
+ """Implementation of a calculated sensor for dew point."""
120
+
121
+ __slots__ = ()
122
+
123
+ _calculated_parameter = CalulatedParameter.DEW_POINT
124
+
125
+ def __init__(self, *, channel: hmd.Channel) -> None:
126
+ """Initialize the data point."""
127
+ super().__init__(channel=channel)
128
+ self._unit = "°C"
129
+
130
+ @staticmethod
131
+ def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
132
+ """Return if this calculated data point is relevant for the model."""
133
+ return _is_relevant_for_model_temperature_and_humidity(channel=channel)
134
+
135
+ @state_property
136
+ def value(self) -> float | None:
137
+ """Return the value."""
138
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
139
+ return calculate_dew_point(
140
+ temperature=self._dp_temperature.value,
141
+ humidity=self._dp_humidity.value,
142
+ )
143
+ return None
144
+
145
+
146
+ class DewPointSpread(BaseClimateSensor):
147
+ """Implementation of a calculated sensor for dew point spread."""
148
+
149
+ __slots__ = ()
150
+
151
+ _calculated_parameter = CalulatedParameter.DEW_POINT_SPREAD
152
+
153
+ def __init__(self, *, channel: hmd.Channel) -> None:
154
+ """Initialize the data point."""
155
+ super().__init__(channel=channel)
156
+ self._unit = "K"
157
+
158
+ @staticmethod
159
+ def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
160
+ """Return if this calculated data point is relevant for the model."""
161
+ return _is_relevant_for_model_temperature_and_humidity(channel=channel)
162
+
163
+ @state_property
164
+ def value(self) -> float | None:
165
+ """Return the value."""
166
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
167
+ return calculate_dew_point_spread(
168
+ temperature=self._dp_temperature.value,
169
+ humidity=self._dp_humidity.value,
170
+ )
171
+ return None
172
+
173
+
174
+ class Enthalpy(BaseClimateSensor):
175
+ """Implementation of a calculated sensor for enthalpy."""
176
+
177
+ __slots__ = ()
178
+
179
+ _calculated_parameter = CalulatedParameter.ENTHALPY
180
+
181
+ def __init__(self, *, channel: hmd.Channel) -> None:
182
+ """Initialize the data point."""
183
+ super().__init__(channel=channel)
184
+ self._unit = "kJ/kg"
185
+
186
+ @staticmethod
187
+ def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
188
+ """Return if this calculated data point is relevant for the model."""
189
+ return _is_relevant_for_model_temperature_and_humidity(channel=channel)
190
+
191
+ @state_property
192
+ def value(self) -> float | None:
193
+ """Return the value."""
194
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
195
+ return calculate_enthalpy(
196
+ temperature=self._dp_temperature.value,
197
+ humidity=self._dp_humidity.value,
198
+ )
199
+ return None
200
+
201
+
202
+ class FrostPoint(BaseClimateSensor):
203
+ """Implementation of a calculated sensor for frost point."""
204
+
205
+ __slots__ = ()
206
+
207
+ _calculated_parameter = CalulatedParameter.FROST_POINT
208
+
209
+ def __init__(self, *, channel: hmd.Channel) -> None:
210
+ """Initialize the data point."""
211
+ super().__init__(channel=channel)
212
+ self._unit = "°C"
213
+
214
+ @staticmethod
215
+ def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
216
+ """Return if this calculated data point is relevant for the model."""
217
+ return _is_relevant_for_model_temperature_and_humidity(
218
+ channel=channel, relevant_models=_RELEVANT_MODELS_FROST_POINT
219
+ )
220
+
221
+ @state_property
222
+ def value(self) -> float | None:
223
+ """Return the value."""
224
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
225
+ return calculate_frost_point(
226
+ temperature=self._dp_temperature.value,
227
+ humidity=self._dp_humidity.value,
228
+ )
229
+ return None
230
+
231
+
232
+ class VaporConcentration(BaseClimateSensor):
233
+ """Implementation of a calculated sensor for vapor concentration."""
234
+
235
+ __slots__ = ()
236
+
237
+ _calculated_parameter = CalulatedParameter.VAPOR_CONCENTRATION
238
+
239
+ def __init__(self, *, channel: hmd.Channel) -> None:
240
+ """Initialize the data point."""
241
+ super().__init__(channel=channel)
242
+ self._unit = "g/m³"
243
+
244
+ @staticmethod
245
+ def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
246
+ """Return if this calculated data point is relevant for the model."""
247
+ return _is_relevant_for_model_temperature_and_humidity(channel=channel)
248
+
249
+ @state_property
250
+ def value(self) -> float | None:
251
+ """Return the value."""
252
+ if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
253
+ return calculate_vapor_concentration(
254
+ temperature=self._dp_temperature.value,
255
+ humidity=self._dp_humidity.value,
256
+ )
257
+ return None
258
+
259
+
260
+ def _is_relevant_for_model_temperature_and_humidity(
261
+ channel: hmd.Channel, relevant_models: tuple[str, ...] | None = None
262
+ ) -> bool:
263
+ """Return if this calculated data point is relevant for the model with temperature and humidity."""
264
+ return (
265
+ (
266
+ relevant_models is not None
267
+ and element_matches_key(search_elements=relevant_models, compare_with=channel.device.model)
268
+ )
269
+ or relevant_models is None
270
+ ) and (
271
+ (
272
+ channel.get_generic_data_point(parameter=Parameter.TEMPERATURE, paramset_key=ParamsetKey.VALUES) is not None
273
+ or channel.get_generic_data_point(parameter=Parameter.ACTUAL_TEMPERATURE, paramset_key=ParamsetKey.VALUES)
274
+ is not None
275
+ )
276
+ and (
277
+ channel.get_generic_data_point(parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES) is not None
278
+ or channel.get_generic_data_point(parameter=Parameter.ACTUAL_HUMIDITY, paramset_key=ParamsetKey.VALUES)
279
+ is not None
280
+ )
281
+ )
282
+
283
+
284
+ _RELEVANT_MODELS_APPARENT_TEMPERATURE: Final[tuple[str, ...]] = ("HmIP-SWO",)
285
+
286
+
287
+ _RELEVANT_MODELS_FROST_POINT: Final[tuple[str, ...]] = (
288
+ "HmIP-STHO",
289
+ "HmIP-SWO",
290
+ )