aiohomematic 2025.10.6__tar.gz → 2025.10.8__tar.gz

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 (113) hide show
  1. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/PKG-INFO +1 -1
  2. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/central/__init__.py +1 -1
  3. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/central/rpc_server.py +7 -5
  4. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/client/__init__.py +1 -1
  5. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/const.py +3 -1
  6. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/calculated/__init__.py +20 -3
  7. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/calculated/climate.py +58 -0
  8. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/calculated/support.py +40 -2
  9. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/device.py +1 -4
  10. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/support.py +4 -5
  11. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic.egg-info/PKG-INFO +1 -1
  12. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_central.py +7 -7
  13. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_central_pydevccu.py +4 -4
  14. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/LICENSE +0 -0
  15. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/README.md +0 -0
  16. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/__init__.py +0 -0
  17. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/async_support.py +0 -0
  18. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/caches/__init__.py +0 -0
  19. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/caches/dynamic.py +0 -0
  20. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/caches/persistent.py +0 -0
  21. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/caches/visibility.py +0 -0
  22. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/central/decorators.py +0 -0
  23. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/client/_rpc_errors.py +0 -0
  24. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/client/json_rpc.py +0 -0
  25. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/client/rpc_proxy.py +0 -0
  26. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/context.py +0 -0
  27. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/converter.py +0 -0
  28. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/decorators.py +0 -0
  29. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/exceptions.py +0 -0
  30. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/hmcli.py +0 -0
  31. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/__init__.py +0 -0
  32. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/calculated/data_point.py +0 -0
  33. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/calculated/operating_voltage_level.py +0 -0
  34. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/custom/__init__.py +0 -0
  35. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/custom/climate.py +0 -0
  36. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/custom/const.py +0 -0
  37. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/custom/cover.py +0 -0
  38. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/custom/data_point.py +0 -0
  39. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/custom/definition.py +0 -0
  40. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/custom/light.py +0 -0
  41. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/custom/lock.py +0 -0
  42. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/custom/siren.py +0 -0
  43. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/custom/support.py +0 -0
  44. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/custom/switch.py +0 -0
  45. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/custom/valve.py +0 -0
  46. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/data_point.py +0 -0
  47. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/event.py +0 -0
  48. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/generic/__init__.py +0 -0
  49. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/generic/action.py +0 -0
  50. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/generic/binary_sensor.py +0 -0
  51. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/generic/button.py +0 -0
  52. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/generic/data_point.py +0 -0
  53. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/generic/number.py +0 -0
  54. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/generic/select.py +0 -0
  55. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/generic/sensor.py +0 -0
  56. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/generic/switch.py +0 -0
  57. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/generic/text.py +0 -0
  58. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/hub/__init__.py +0 -0
  59. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/hub/binary_sensor.py +0 -0
  60. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/hub/button.py +0 -0
  61. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/hub/data_point.py +0 -0
  62. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/hub/number.py +0 -0
  63. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/hub/select.py +0 -0
  64. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/hub/sensor.py +0 -0
  65. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/hub/switch.py +0 -0
  66. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/hub/text.py +0 -0
  67. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/support.py +0 -0
  68. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/model/update.py +0 -0
  69. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/property_decorators.py +0 -0
  70. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/py.typed +0 -0
  71. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/rega_scripts/fetch_all_device_data.fn +0 -0
  72. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/rega_scripts/get_program_descriptions.fn +0 -0
  73. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/rega_scripts/get_serial.fn +0 -0
  74. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/rega_scripts/get_system_variable_descriptions.fn +0 -0
  75. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/rega_scripts/set_program_state.fn +0 -0
  76. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/rega_scripts/set_system_variable.fn +0 -0
  77. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic/validator.py +0 -0
  78. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic.egg-info/SOURCES.txt +0 -0
  79. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic.egg-info/dependency_links.txt +0 -0
  80. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic.egg-info/requires.txt +0 -0
  81. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic.egg-info/top_level.txt +0 -0
  82. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic_support/__init__.py +0 -0
  83. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/aiohomematic_support/client_local.py +0 -0
  84. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/pyproject.toml +0 -0
  85. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/setup.cfg +0 -0
  86. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_action.py +0 -0
  87. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_async_support.py +0 -0
  88. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_binary_sensor.py +0 -0
  89. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_button.py +0 -0
  90. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_calculated_support.py +0 -0
  91. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_climate.py +0 -0
  92. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_cover.py +0 -0
  93. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_decorator.py +0 -0
  94. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_device.py +0 -0
  95. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_dynamic_caches.py +0 -0
  96. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_entity.py +0 -0
  97. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_event.py +0 -0
  98. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_json_rpc.py +0 -0
  99. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_json_rpc_client_integration.py +0 -0
  100. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_kwonly_lint.py +0 -0
  101. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_light.py +0 -0
  102. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_lock.py +0 -0
  103. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_logging_support.py +0 -0
  104. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_number.py +0 -0
  105. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_select.py +0 -0
  106. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_sensor.py +0 -0
  107. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_siren.py +0 -0
  108. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_support.py +0 -0
  109. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_support_extra.py +0 -0
  110. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_switch.py +0 -0
  111. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_text.py +0 -0
  112. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_valve.py +0 -0
  113. {aiohomematic-2025.10.6 → aiohomematic-2025.10.8}/tests/test_xml_rpc_proxy_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic
3
- Version: 2025.10.6
3
+ Version: 2025.10.8
4
4
  Summary: Homematic interface for Home Assistant running on Python 3.
5
5
  Home-page: https://github.com/sukramj/aiohomematic
6
6
  Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
@@ -1068,7 +1068,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1068
1068
  )
1069
1069
  return
1070
1070
  client = self._clients[interface_id]
1071
- if (device_descriptions := await client.get_all_device_description(device_address=address)) is None:
1071
+ if (device_descriptions := await client.get_all_device_descriptions(device_address=address)) is None:
1072
1072
  _LOGGER.warning(
1073
1073
  "ADD_NEW_DEVICES_MANUALLY failed: No device description found for address %s on interface_id %s",
1074
1074
  address,
@@ -12,7 +12,7 @@ from __future__ import annotations
12
12
  import contextlib
13
13
  import logging
14
14
  import threading
15
- from typing import Any, Final
15
+ from typing import Any, Final, cast
16
16
  from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
17
17
 
18
18
  from aiohomematic import central as hmcu
@@ -175,7 +175,7 @@ class RpcServer(threading.Thread):
175
175
  """RPC server thread to handle messages from the backend."""
176
176
 
177
177
  _initialized: bool = False
178
- _instances: Final[dict[tuple[str, int], XmlRpcServer]] = {}
178
+ _instances: Final[dict[tuple[str, int], RpcServer]] = {}
179
179
 
180
180
  def __init__(
181
181
  self,
@@ -190,9 +190,10 @@ class RpcServer(threading.Thread):
190
190
  self._listen_ip_addr: Final = ip_addr
191
191
  self._listen_port: Final[int] = find_free_port() if port == PORT_ANY else port
192
192
  self._address: Final[tuple[str, int]] = (ip_addr, self._listen_port)
193
- threading.Thread.__init__(self, name=f"RpcServer {ip_addr}:{self._listen_port}")
194
193
  self._centrals: Final[dict[str, hmcu.CentralUnit]] = {}
195
194
  self._simple_rpc_server: SimpleXMLRPCServer
195
+ self._instances[self._address] = self
196
+ threading.Thread.__init__(self, name=f"RpcServer {ip_addr}:{self._listen_port}")
196
197
 
197
198
  def run(self) -> None:
198
199
  """Run the RPC-Server thread."""
@@ -266,8 +267,9 @@ class XmlRpcServer(RpcServer):
266
267
  ) -> None:
267
268
  """Init XmlRPC server."""
268
269
 
270
+ if self._initialized:
271
+ return
269
272
  super().__init__(ip_addr=ip_addr, port=port)
270
- self._instances[self._address] = self
271
273
  self._simple_rpc_server = HomematicXMLRPCServer(
272
274
  addr=self._address,
273
275
  requestHandler=RequestHandler,
@@ -283,7 +285,7 @@ class XmlRpcServer(RpcServer):
283
285
  if (rpc := cls._instances.get((ip_addr, port))) is None:
284
286
  _LOGGER.debug("Creating XmlRpc server")
285
287
  return super().__new__(cls)
286
- return rpc
288
+ return cast(XmlRpcServer, rpc)
287
289
 
288
290
 
289
291
  def create_xml_rpc_server(*, ip_addr: str = IP_ANY_V4, port: int = PORT_ANY) -> XmlRpcServer:
@@ -507,7 +507,7 @@ class Client(ABC, LogContextMixin):
507
507
  return None
508
508
 
509
509
  @inspector(re_raise=False)
510
- async def get_all_device_description(self, *, device_address: str) -> tuple[DeviceDescription, ...] | None:
510
+ async def get_all_device_descriptions(self, *, device_address: str) -> tuple[DeviceDescription, ...] | None:
511
511
  """Get all device descriptions from the backend."""
512
512
  all_device_description: list[DeviceDescription] = []
513
513
  if main_dd := await self.get_device_description(device_address=device_address):
@@ -19,7 +19,7 @@ import sys
19
19
  from types import MappingProxyType
20
20
  from typing import Any, Final, NamedTuple, Required, TypeAlias, TypedDict
21
21
 
22
- VERSION: Final = "2025.10.6"
22
+ VERSION: Final = "2025.10.8"
23
23
 
24
24
  # Detect test speedup mode via environment
25
25
  _TEST_SPEEDUP: Final = (
@@ -179,6 +179,8 @@ class CalulatedParameter(StrEnum):
179
179
 
180
180
  APPARENT_TEMPERATURE = "APPARENT_TEMPERATURE"
181
181
  DEW_POINT = "DEW_POINT"
182
+ DEW_POINT_SPREAD = "DEW_POINT_SPREAD"
183
+ ENTHALPY = "ENTHALPY"
182
184
  FROST_POINT = "FROST_POINT"
183
185
  OPERATING_VOLTAGE_LEVEL = "OPERATING_VOLTAGE_LEVEL"
184
186
  VAPOR_CONCENTRATION = "VAPOR_CONCENTRATION"
@@ -24,7 +24,7 @@ Factory:
24
24
  normal read-only data points.
25
25
 
26
26
  Modules/classes:
27
- - ApparentTemperature, DewPoint, FrostPoint, VaporConcentration: Climate-related
27
+ - ApparentTemperature, DewPoint, DewPointSpread, Enthalphy, FrostPoint, VaporConcentration: Climate-related
28
28
  sensors implemented in climate.py using well-known formulas (see
29
29
  aiohomematic.model.calculated.support for details and references).
30
30
  - OperatingVoltageLevel: Interprets battery/voltage values and exposes a human
@@ -41,7 +41,14 @@ from typing import Final
41
41
 
42
42
  from aiohomematic.decorators import inspector
43
43
  from aiohomematic.model import device as hmd
44
- from aiohomematic.model.calculated.climate import ApparentTemperature, DewPoint, FrostPoint, VaporConcentration
44
+ from aiohomematic.model.calculated.climate import (
45
+ ApparentTemperature,
46
+ DewPoint,
47
+ DewPointSpread,
48
+ Enthalpy,
49
+ FrostPoint,
50
+ VaporConcentration,
51
+ )
45
52
  from aiohomematic.model.calculated.data_point import CalculatedDataPoint
46
53
  from aiohomematic.model.calculated.operating_voltage_level import OperatingVoltageLevel
47
54
 
@@ -49,13 +56,23 @@ __all__ = [
49
56
  "ApparentTemperature",
50
57
  "CalculatedDataPoint",
51
58
  "DewPoint",
59
+ "DewPointSpread",
60
+ "Enthalpy",
52
61
  "FrostPoint",
53
62
  "OperatingVoltageLevel",
54
63
  "VaporConcentration",
55
64
  "create_calculated_data_points",
56
65
  ]
57
66
 
58
- _CALCULATED_DATA_POINTS: Final = (ApparentTemperature, DewPoint, FrostPoint, OperatingVoltageLevel, VaporConcentration)
67
+ _CALCULATED_DATA_POINTS: Final = (
68
+ ApparentTemperature,
69
+ DewPoint,
70
+ DewPointSpread,
71
+ Enthalpy,
72
+ FrostPoint,
73
+ OperatingVoltageLevel,
74
+ VaporConcentration,
75
+ )
59
76
  _LOGGER: Final = logging.getLogger(__name__)
60
77
 
61
78
 
@@ -13,6 +13,8 @@ from aiohomematic.model.calculated.data_point import CalculatedDataPoint
13
13
  from aiohomematic.model.calculated.support import (
14
14
  calculate_apparent_temperature,
15
15
  calculate_dew_point,
16
+ calculate_dew_point_spread,
17
+ calculate_enthalpy,
16
18
  calculate_frost_point,
17
19
  calculate_vapor_concentration,
18
20
  )
@@ -141,6 +143,62 @@ class DewPoint(BaseClimateSensor):
141
143
  return None
142
144
 
143
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
+
144
202
  class FrostPoint(BaseClimateSensor):
145
203
  """Implementation of a calculated sensor for frost point."""
146
204
 
@@ -16,9 +16,47 @@ from typing import Final
16
16
 
17
17
  from aiohomematic.support import extract_exc_args
18
18
 
19
+ _DEFAULT_PRESSURE_HPA: Final = 1013.25
19
20
  _LOGGER: Final = logging.getLogger(__name__)
20
21
 
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
+
22
60
  def _calculate_heat_index(*, temperature: float, humidity: int) -> float:
23
61
  """
24
62
  Calculate the Heat Index (feels like temperature) based on the NOAA equation.
@@ -158,10 +196,10 @@ def calculate_dew_point(*, temperature: float, humidity: int) -> float | None:
158
196
  def calculate_frost_point(*, temperature: float, humidity: int) -> float | None:
159
197
  """Calculate the frost point."""
160
198
  try:
161
- if (dewpoint := calculate_dew_point(temperature=temperature, humidity=humidity)) is None:
199
+ if (dew_point := calculate_dew_point(temperature=temperature, humidity=humidity)) is None:
162
200
  return None
163
201
  t = temperature + 273.15
164
- td = dewpoint + 273.15
202
+ td = dew_point + 273.15
165
203
 
166
204
  return round((td + (2671.02 / ((2954.61 / t) + 2.193665 * math.log(t) - 13.3448)) - t) - 273.15, 1)
167
205
  except ValueError as verr:
@@ -1338,10 +1338,7 @@ class _DefinitionExporter:
1338
1338
  def perform_save() -> DataOperationResult:
1339
1339
  if not check_or_create_directory(directory=file_dir):
1340
1340
  return DataOperationResult.NO_SAVE # pragma: no cover
1341
- with open(
1342
- file=os.path.join(file_dir, filename),
1343
- mode="wb",
1344
- ) as fptr:
1341
+ with open(file=os.path.join(file_dir, filename), mode="wb") as fptr:
1345
1342
  fptr.write(orjson.dumps(data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS))
1346
1343
  return DataOperationResult.SAVE_SUCCESS
1347
1344
 
@@ -43,6 +43,7 @@ from aiohomematic.const import (
43
43
  NO_CACHE_ENTRY,
44
44
  PRIMARY_CLIENT_CANDIDATE_INTERFACES,
45
45
  TIMEOUT,
46
+ UTF_8,
46
47
  CommandRxMode,
47
48
  DeviceDescription,
48
49
  ParamsetKey,
@@ -526,9 +527,9 @@ def hash_sha256(*, value: Any) -> str:
526
527
  data = orjson.dumps(value, option=orjson.OPT_SORT_KEYS | orjson.OPT_NON_STR_KEYS)
527
528
  except Exception:
528
529
  # Fallback: convert to a hashable representation and use repr()
529
- data = repr(_make_value_hashable(value=value)).encode()
530
+ data = repr(_make_value_hashable(value=value)).encode(encoding=UTF_8)
530
531
  hasher.update(data)
531
- return base64.b64encode(hasher.digest()).decode()
532
+ return base64.b64encode(hasher.digest()).decode(encoding=UTF_8)
532
533
 
533
534
 
534
535
  def _make_value_hashable(*, value: Any) -> Any:
@@ -632,9 +633,7 @@ def log_boundary_error(
632
633
  log_message += f" {message}"
633
634
 
634
635
  if log_context:
635
- log_message += (
636
- f" ctx={orjson.dumps(_safe_log_context(context=log_context), option=orjson.OPT_SORT_KEYS).decode()}"
637
- )
636
+ log_message += f" ctx={orjson.dumps(_safe_log_context(context=log_context), option=orjson.OPT_SORT_KEYS).decode(encoding=UTF_8)}"
638
637
 
639
638
  # Choose level if not provided:
640
639
  if (chosen_level := level) is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic
3
- Version: 2025.10.6
3
+ Version: 2025.10.8
4
4
  Summary: Homematic interface for Home Assistant running on Python 3.
5
5
  Home-page: https://github.com/sukramj/aiohomematic
6
6
  Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
@@ -519,7 +519,7 @@ async def test_data_points_by_category(
519
519
  central, _, _ = central_client_factory
520
520
  ebp_sensor = central.get_data_points(category=DataPointCategory.SENSOR)
521
521
  assert ebp_sensor
522
- assert len(ebp_sensor) == 16
522
+ assert len(ebp_sensor) == 18
523
523
 
524
524
  def _device_changed(self, *args: Any, **kwargs: Any) -> None:
525
525
  """Handle device state changes."""
@@ -527,7 +527,7 @@ async def test_data_points_by_category(
527
527
  ebp_sensor[0].register_data_point_updated_callback(cb=_device_changed, custom_id="some_id")
528
528
  ebp_sensor2 = central.get_data_points(category=DataPointCategory.SENSOR, registered=False)
529
529
  assert ebp_sensor2
530
- assert len(ebp_sensor2) == 15
530
+ assert len(ebp_sensor2) == 17
531
531
 
532
532
 
533
533
  @pytest.mark.asyncio
@@ -595,13 +595,13 @@ async def test_add_device(
595
595
  """Test add_device."""
596
596
  central, _, _ = central_client_factory
597
597
  assert len(central._devices) == 1
598
- assert len(central.get_data_points(exclude_no_create=False)) == 31
598
+ assert len(central.get_data_points(exclude_no_create=False)) == 33
599
599
  assert len(central.device_descriptions._raw_device_descriptions.get(const.INTERFACE_ID)) == 9
600
600
  assert len(central.paramset_descriptions._raw_paramset_descriptions.get(const.INTERFACE_ID)) == 9
601
601
  dev_desc = helper.load_device_description(central=central, filename="HmIP-BSM.json")
602
602
  await central.add_new_devices(interface_id=const.INTERFACE_ID, device_descriptions=dev_desc)
603
603
  assert len(central._devices) == 2
604
- assert len(central.get_data_points(exclude_no_create=False)) == 62
604
+ assert len(central.get_data_points(exclude_no_create=False)) == 64
605
605
  assert len(central.device_descriptions._raw_device_descriptions.get(const.INTERFACE_ID)) == 20
606
606
  assert len(central.paramset_descriptions._raw_paramset_descriptions.get(const.INTERFACE_ID)) == 20
607
607
  await central.add_new_devices(interface_id="NOT_ANINTERFACE_ID", device_descriptions=dev_desc)
@@ -629,13 +629,13 @@ async def test_delete_device(
629
629
  """Test device delete_device."""
630
630
  central, _, _ = central_client_factory
631
631
  assert len(central._devices) == 2
632
- assert len(central.get_data_points(exclude_no_create=False)) == 62
632
+ assert len(central.get_data_points(exclude_no_create=False)) == 64
633
633
  assert len(central.device_descriptions._raw_device_descriptions.get(const.INTERFACE_ID)) == 20
634
634
  assert len(central.paramset_descriptions._raw_paramset_descriptions.get(const.INTERFACE_ID)) == 20
635
635
 
636
636
  await central.delete_devices(interface_id=const.INTERFACE_ID, addresses=["VCU2128127"])
637
637
  assert len(central._devices) == 1
638
- assert len(central.get_data_points(exclude_no_create=False)) == 31
638
+ assert len(central.get_data_points(exclude_no_create=False)) == 33
639
639
  assert len(central.device_descriptions._raw_device_descriptions.get(const.INTERFACE_ID)) == 9
640
640
  assert len(central.paramset_descriptions._raw_paramset_descriptions.get(const.INTERFACE_ID)) == 9
641
641
 
@@ -856,7 +856,7 @@ async def test_central_direct(factory: helper.Factory) -> None:
856
856
  assert central.available is False
857
857
  assert central.system_information.serial == "0815_4711"
858
858
  assert len(central._devices) == 2
859
- assert len(central.get_data_points(exclude_no_create=False)) == 62
859
+ assert len(central.get_data_points(exclude_no_create=False)) == 64
860
860
  finally:
861
861
  await central.stop()
862
862
 
@@ -27,7 +27,7 @@ async def test_central_mini(central_unit_mini) -> None:
27
27
  assert central_unit_mini.get_client(interface_id=const.INTERFACE_ID).model == "PyDevCCU"
28
28
  assert central_unit_mini.primary_client.model == "PyDevCCU"
29
29
  assert len(central_unit_mini._devices) == 2
30
- assert len(central_unit_mini.get_data_points(exclude_no_create=False)) == 68
30
+ assert len(central_unit_mini.get_data_points(exclude_no_create=False)) == 70
31
31
 
32
32
  usage_types: dict[DataPointUsage, int] = {}
33
33
  for dp in central_unit_mini.get_data_points(exclude_no_create=False):
@@ -39,7 +39,7 @@ async def test_central_mini(central_unit_mini) -> None:
39
39
 
40
40
  assert usage_types[DataPointUsage.NO_CREATE] == 45
41
41
  assert usage_types[DataPointUsage.CDP_PRIMARY] == 4
42
- assert usage_types[DataPointUsage.DATA_POINT] == 14
42
+ assert usage_types[DataPointUsage.DATA_POINT] == 16
43
43
  assert usage_types[DataPointUsage.CDP_VISIBLE] == 5
44
44
 
45
45
 
@@ -193,12 +193,12 @@ async def test_central_full(central_unit_full) -> None: # noqa: C901
193
193
  assert usage_types[DataPointUsage.CDP_PRIMARY] == 272
194
194
  assert usage_types[DataPointUsage.CDP_SECONDARY] == 162
195
195
  assert usage_types[DataPointUsage.CDP_VISIBLE] == 141
196
- assert usage_types[DataPointUsage.DATA_POINT] == 3937
196
+ assert usage_types[DataPointUsage.DATA_POINT] == 4033
197
197
  assert usage_types[DataPointUsage.NO_CREATE] == 4291
198
198
 
199
199
  assert len(ce_channels) == 130
200
200
  assert len(data_point_types) == 6
201
- assert len(parameters) == 232
201
+ assert len(parameters) == 234
202
202
 
203
203
  assert len(central_unit_full._devices) == 394
204
204
  virtual_remotes = ["VCU4264293", "VCU0000057", "VCU0000001"]