aiohomematic 2025.9.8__tar.gz → 2025.10.0__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 (106) hide show
  1. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/PKG-INFO +1 -1
  2. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/central/__init__.py +3 -4
  3. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/client/xml_rpc.py +1 -1
  4. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/const.py +2 -1
  5. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/custom/climate.py +85 -12
  6. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/data_point.py +1 -1
  7. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/generic/data_point.py +2 -0
  8. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic.egg-info/PKG-INFO +1 -1
  9. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_climate.py +68 -9
  10. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/LICENSE +0 -0
  11. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/README.md +0 -0
  12. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/__init__.py +0 -0
  13. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/async_support.py +0 -0
  14. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/caches/__init__.py +0 -0
  15. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/caches/dynamic.py +0 -0
  16. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/caches/persistent.py +0 -0
  17. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/caches/visibility.py +0 -0
  18. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/central/decorators.py +0 -0
  19. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/central/xml_rpc_server.py +0 -0
  20. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/client/__init__.py +0 -0
  21. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/client/_rpc_errors.py +0 -0
  22. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/client/json_rpc.py +0 -0
  23. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/context.py +0 -0
  24. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/converter.py +0 -0
  25. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/decorators.py +0 -0
  26. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/exceptions.py +0 -0
  27. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/hmcli.py +0 -0
  28. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/__init__.py +0 -0
  29. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/calculated/__init__.py +0 -0
  30. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/calculated/climate.py +0 -0
  31. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/calculated/data_point.py +0 -0
  32. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/calculated/operating_voltage_level.py +0 -0
  33. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/calculated/support.py +0 -0
  34. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/custom/__init__.py +0 -0
  35. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/custom/const.py +0 -0
  36. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/custom/cover.py +0 -0
  37. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/custom/data_point.py +0 -0
  38. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/custom/definition.py +0 -0
  39. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/custom/light.py +0 -0
  40. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/custom/lock.py +0 -0
  41. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/custom/siren.py +0 -0
  42. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/custom/support.py +0 -0
  43. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/custom/switch.py +0 -0
  44. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/custom/valve.py +0 -0
  45. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/device.py +0 -0
  46. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/event.py +0 -0
  47. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/generic/__init__.py +0 -0
  48. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/generic/action.py +0 -0
  49. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/generic/binary_sensor.py +0 -0
  50. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/generic/button.py +0 -0
  51. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/generic/number.py +0 -0
  52. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/generic/select.py +0 -0
  53. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/generic/sensor.py +0 -0
  54. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/generic/switch.py +0 -0
  55. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/generic/text.py +0 -0
  56. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/hub/__init__.py +0 -0
  57. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/hub/binary_sensor.py +0 -0
  58. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/hub/button.py +0 -0
  59. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/hub/data_point.py +0 -0
  60. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/hub/number.py +0 -0
  61. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/hub/select.py +0 -0
  62. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/hub/sensor.py +0 -0
  63. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/hub/switch.py +0 -0
  64. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/hub/text.py +0 -0
  65. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/support.py +0 -0
  66. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/model/update.py +0 -0
  67. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/property_decorators.py +0 -0
  68. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/py.typed +0 -0
  69. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/rega_scripts/fetch_all_device_data.fn +0 -0
  70. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/rega_scripts/get_program_descriptions.fn +0 -0
  71. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/rega_scripts/get_serial.fn +0 -0
  72. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/rega_scripts/get_system_variable_descriptions.fn +0 -0
  73. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/rega_scripts/set_program_state.fn +0 -0
  74. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/rega_scripts/set_system_variable.fn +0 -0
  75. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/support.py +0 -0
  76. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic/validator.py +0 -0
  77. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic.egg-info/SOURCES.txt +0 -0
  78. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic.egg-info/dependency_links.txt +0 -0
  79. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic.egg-info/requires.txt +0 -0
  80. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic.egg-info/top_level.txt +0 -0
  81. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic_support/__init__.py +0 -0
  82. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/aiohomematic_support/client_local.py +0 -0
  83. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/pyproject.toml +0 -0
  84. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/setup.cfg +0 -0
  85. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_action.py +0 -0
  86. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_binary_sensor.py +0 -0
  87. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_button.py +0 -0
  88. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_calculated_support.py +0 -0
  89. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_central.py +0 -0
  90. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_central_pydevccu.py +0 -0
  91. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_cover.py +0 -0
  92. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_decorator.py +0 -0
  93. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_device.py +0 -0
  94. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_entity.py +0 -0
  95. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_event.py +0 -0
  96. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_json_rpc.py +0 -0
  97. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_light.py +0 -0
  98. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_lock.py +0 -0
  99. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_number.py +0 -0
  100. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_select.py +0 -0
  101. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_sensor.py +0 -0
  102. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_siren.py +0 -0
  103. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_support.py +0 -0
  104. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_switch.py +0 -0
  105. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_text.py +0 -0
  106. {aiohomematic-2025.9.8 → aiohomematic-2025.10.0}/tests/test_valve.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic
3
- Version: 2025.9.8
3
+ Version: 2025.10.0
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>
@@ -44,12 +44,10 @@ Example (simplified):
44
44
 
45
45
  cfg = CentralConfig(
46
46
  central_id="ccu-main",
47
- default_callback_port=43439,
48
47
  host="ccu.local",
49
48
  interface_configs=iface_cfgs,
50
49
  name="MyCCU",
51
50
  password="secret",
52
- storage_folder=".storage",
53
51
  username="admin",
54
52
  )
55
53
 
@@ -102,6 +100,7 @@ from aiohomematic.const import (
102
100
  DEFAULT_MAX_READ_WORKERS,
103
101
  DEFAULT_PERIODIC_REFRESH_INTERVAL,
104
102
  DEFAULT_PROGRAM_MARKERS,
103
+ DEFAULT_STORAGE_FOLDER,
105
104
  DEFAULT_SYS_SCAN_INTERVAL,
106
105
  DEFAULT_SYSVAR_MARKERS,
107
106
  DEFAULT_TLS,
@@ -1828,16 +1827,15 @@ class CentralConfig:
1828
1827
  def __init__(
1829
1828
  self,
1830
1829
  central_id: str,
1831
- default_callback_port: int,
1832
1830
  host: str,
1833
1831
  interface_configs: AbstractSet[hmcl.InterfaceConfig],
1834
1832
  name: str,
1835
1833
  password: str,
1836
- storage_folder: str,
1837
1834
  username: str,
1838
1835
  client_session: ClientSession | None = None,
1839
1836
  callback_host: str | None = None,
1840
1837
  callback_port: int | None = None,
1838
+ default_callback_port: int = PORT_ANY,
1841
1839
  enable_device_firmware_check: bool = DEFAULT_ENABLE_DEVICE_FIRMWARE_CHECK,
1842
1840
  enable_program_scan: bool = DEFAULT_ENABLE_PROGRAM_SCAN,
1843
1841
  enable_sysvar_scan: bool = DEFAULT_ENABLE_SYSVAR_SCAN,
@@ -1851,6 +1849,7 @@ class CentralConfig:
1851
1849
  periodic_refresh_interval: int = DEFAULT_PERIODIC_REFRESH_INTERVAL,
1852
1850
  program_markers: tuple[DescriptionMarker | str, ...] = DEFAULT_PROGRAM_MARKERS,
1853
1851
  start_direct: bool = False,
1852
+ storage_folder: str = DEFAULT_STORAGE_FOLDER,
1854
1853
  sys_scan_interval: int = DEFAULT_SYS_SCAN_INTERVAL,
1855
1854
  sysvar_markers: tuple[DescriptionMarker | str, ...] = DEFAULT_SYSVAR_MARKERS,
1856
1855
  tls: bool = DEFAULT_TLS,
@@ -203,7 +203,7 @@ class XmlRpcProxy(xmlrpc.client.ServerProxy):
203
203
  if not self._connection_state.has_issue(issuer=self, iid=self._interface_id):
204
204
  if perr.errmsg == "Unauthorized":
205
205
  raise AuthFailure(perr) from perr
206
- raise NoConnectionException(perr.errmsg) from perr
206
+ raise NoConnectionException(f"No connection to {self.log_context} ({perr.errmsg})") from perr
207
207
  except Exception as exc:
208
208
  raise ClientException(exc) from exc
209
209
 
@@ -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.9.8"
22
+ VERSION: Final = "2025.10.0"
23
23
 
24
24
  # Detect test speedup mode via environment
25
25
  _TEST_SPEEDUP: Final = (
@@ -27,6 +27,7 @@ _TEST_SPEEDUP: Final = (
27
27
  )
28
28
 
29
29
  # default
30
+ DEFAULT_STORAGE_FOLDER: Final = "aiohomematic_storage"
30
31
  DEFAULT_CUSTOM_ID: Final = "custom_id"
31
32
  DEFAULT_ENABLE_DEVICE_FIRMWARE_CHECK: Final = False
32
33
  DEFAULT_ENABLE_PROGRAM_SCAN: Final = True
@@ -4,6 +4,7 @@
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ from abc import abstractmethod
7
8
  from collections.abc import Mapping
8
9
  from datetime import datetime, timedelta
9
10
  from enum import IntEnum, StrEnum
@@ -26,7 +27,16 @@ from aiohomematic.model.custom.const import DeviceProfile, Field
26
27
  from aiohomematic.model.custom.data_point import CustomDataPoint
27
28
  from aiohomematic.model.custom.support import CustomConfig
28
29
  from aiohomematic.model.data_point import CallParameterCollector, bind_collector
29
- from aiohomematic.model.generic import DpAction, DpBinarySensor, DpFloat, DpInteger, DpSelect, DpSensor, DpSwitch
30
+ from aiohomematic.model.generic import (
31
+ DpAction,
32
+ DpBinarySensor,
33
+ DpFloat,
34
+ DpInteger,
35
+ DpSelect,
36
+ DpSensor,
37
+ DpSwitch,
38
+ GenericDataPoint,
39
+ )
30
40
  from aiohomematic.property_decorators import config_property, state_property
31
41
 
32
42
  _LOGGER: Final = logging.getLogger(__name__)
@@ -174,6 +184,7 @@ class BaseCustomDpClimate(CustomDataPoint):
174
184
  "_dp_temperature",
175
185
  "_dp_temperature_maximum",
176
186
  "_dp_temperature_minimum",
187
+ "_old_manu_setpoint",
177
188
  "_supports_schedule",
178
189
  )
179
190
  _category = DataPointCategory.CLIMATE
@@ -199,6 +210,7 @@ class BaseCustomDpClimate(CustomDataPoint):
199
210
  custom_config=custom_config,
200
211
  )
201
212
  self._supports_schedule = False
213
+ self._old_manu_setpoint: float | None = None
202
214
 
203
215
  def _init_data_point_fields(self) -> None:
204
216
  """Init the data_point fields."""
@@ -219,6 +231,13 @@ class BaseCustomDpClimate(CustomDataPoint):
219
231
  self._dp_temperature_minimum: DpFloat = self._get_data_point(
220
232
  field=Field.TEMPERATURE_MINIMUM, data_point_type=DpFloat
221
233
  )
234
+ self._unregister_callbacks.append(
235
+ self._dp_setpoint.register_data_point_updated_callback(cb=self._manu_temp_changed, custom_id="manu_temp")
236
+ )
237
+
238
+ @abstractmethod
239
+ def _manu_temp_changed(self, data_point: GenericDataPoint) -> None:
240
+ """Handle device state changes."""
222
241
 
223
242
  @state_property
224
243
  def current_humidity(self) -> int | None:
@@ -311,11 +330,21 @@ class BaseCustomDpClimate(CustomDataPoint):
311
330
  return _TEMP_CELSIUS
312
331
 
313
332
  @property
314
- def _min_or_target_temperature(self) -> float:
315
- """Return the min or target temperature."""
316
- if (temperature := self.target_temperature or self.min_temp) < self.min_temp:
317
- return self.min_temp
318
- return temperature
333
+ def _temperature_for_heat_mode(self) -> float:
334
+ """
335
+ Return a safe temperature to use when setting mode to HEAT.
336
+
337
+ If the current target temperature is None or represents the special OFF value,
338
+ fall back to the device's minimum valid temperature. Otherwise, return the
339
+ current target temperature clipped to the valid [min, max] range.
340
+ """
341
+ temp = self._old_manu_setpoint or self.target_temperature
342
+ # Treat None or OFF sentinel as invalid/unsafe to restore.
343
+ if temp is None or temp <= _OFF_TEMPERATURE or temp < self.min_temp:
344
+ return self.min_temp if self.min_temp > _OFF_TEMPERATURE else _OFF_TEMPERATURE + 0.5
345
+ if temp > self.max_temp:
346
+ return self.max_temp
347
+ return temp
319
348
 
320
349
  @property
321
350
  def schedule_profile_nos(self) -> int:
@@ -329,10 +358,7 @@ class BaseCustomDpClimate(CustomDataPoint):
329
358
  collector: CallParameterCollector | None = None,
330
359
  do_validate: bool = True,
331
360
  ) -> None:
332
- """Set new target temperature."""
333
- if not self.is_state_change(temperature=temperature):
334
- return
335
-
361
+ """Set new target temperature. The temperature must be set in all cases, even if the values are identical."""
336
362
  if do_validate and self.mode == ClimateMode.HEAT and self.min_max_value_not_relevant_for_manu_mode:
337
363
  do_validate = False
338
364
 
@@ -694,6 +720,9 @@ class CustomDpSimpleRfThermostat(BaseCustomDpClimate):
694
720
 
695
721
  __slots__ = ()
696
722
 
723
+ def _manu_temp_changed(self, data_point: GenericDataPoint) -> None:
724
+ """Handle device state changes."""
725
+
697
726
 
698
727
  class CustomDpRfThermostat(BaseCustomDpClimate):
699
728
  """Classic Homematic thermostat like HM-CC-RT-DN."""
@@ -753,6 +782,28 @@ class CustomDpRfThermostat(BaseCustomDpClimate):
753
782
  field=Field.WEEK_PROGRAM_POINTER, data_point_type=DpSelect
754
783
  )
755
784
 
785
+ self._unregister_callbacks.append(
786
+ self._dp_control_mode.register_data_point_updated_callback(
787
+ cb=self._manu_temp_changed, custom_id="manu_temp"
788
+ )
789
+ )
790
+
791
+ def _manu_temp_changed(self, data_point: GenericDataPoint) -> None:
792
+ """Handle device state changes."""
793
+ if (
794
+ data_point == self._dp_control_mode
795
+ and self.mode == ClimateMode.HEAT
796
+ and self._dp_setpoint.refreshed_recently
797
+ ):
798
+ self._old_manu_setpoint = self.target_temperature
799
+
800
+ if (
801
+ data_point == self._dp_setpoint
802
+ and self.mode == ClimateMode.HEAT
803
+ and self._dp_control_mode.refreshed_recently
804
+ ):
805
+ self._old_manu_setpoint = self.target_temperature
806
+
756
807
  @state_property
757
808
  def activity(self) -> ClimateActivity | None:
758
809
  """Return the current activity."""
@@ -817,7 +868,7 @@ class CustomDpRfThermostat(BaseCustomDpClimate):
817
868
  if mode == ClimateMode.AUTO:
818
869
  await self._dp_auto_mode.send_value(value=True, collector=collector)
819
870
  elif mode == ClimateMode.HEAT:
820
- await self._dp_manu_mode.send_value(value=self._min_or_target_temperature, collector=collector)
871
+ await self._dp_manu_mode.send_value(value=self._temperature_for_heat_mode, collector=collector)
821
872
  elif mode == ClimateMode.OFF:
822
873
  await self._dp_manu_mode.send_value(value=self.target_temperature, collector=collector)
823
874
  # Disable validation here to allow setting a value,
@@ -975,6 +1026,28 @@ class CustomDpIpThermostat(BaseCustomDpClimate):
975
1026
  field=Field.TEMPERATURE_OFFSET, data_point_type=DpFloat
976
1027
  )
977
1028
 
1029
+ self._unregister_callbacks.append(
1030
+ self._dp_set_point_mode.register_data_point_updated_callback(
1031
+ cb=self._manu_temp_changed, custom_id="manu_temp"
1032
+ )
1033
+ )
1034
+
1035
+ def _manu_temp_changed(self, data_point: GenericDataPoint) -> None:
1036
+ """Handle device state changes."""
1037
+ if (
1038
+ data_point == self._dp_set_point_mode
1039
+ and self.mode == ClimateMode.HEAT
1040
+ and self._dp_setpoint.refreshed_recently
1041
+ ):
1042
+ self._old_manu_setpoint = self.target_temperature
1043
+
1044
+ if (
1045
+ data_point == self._dp_setpoint
1046
+ and self.mode == ClimateMode.HEAT
1047
+ and self._dp_set_point_mode.refreshed_recently
1048
+ ):
1049
+ self._old_manu_setpoint = self.target_temperature
1050
+
978
1051
  @property
979
1052
  def _is_heating_mode(self) -> bool:
980
1053
  """Return the heating_mode of the device."""
@@ -1074,7 +1147,7 @@ class CustomDpIpThermostat(BaseCustomDpClimate):
1074
1147
  await self._dp_control_mode.send_value(value=_ModeHmIP.AUTO, collector=collector)
1075
1148
  elif mode in (ClimateMode.HEAT, ClimateMode.COOL):
1076
1149
  await self._dp_control_mode.send_value(value=_ModeHmIP.MANU, collector=collector)
1077
- await self.set_temperature(temperature=self._min_or_target_temperature, collector=collector)
1150
+ await self.set_temperature(temperature=self._temperature_for_heat_mode, collector=collector)
1078
1151
  elif mode == ClimateMode.OFF:
1079
1152
  await self._dp_control_mode.send_value(value=_ModeHmIP.MANU, collector=collector)
1080
1153
  await self.set_temperature(temperature=_OFF_TEMPERATURE, collector=collector, do_validate=False)
@@ -757,7 +757,7 @@ class BaseParameterDataPoint[
757
757
 
758
758
  @property
759
759
  def service(self) -> bool:
760
- """Return the if data_point is visible in the backend."""
760
+ """Return the if data_point is relevant for service messages in the backend."""
761
761
  return self._service
762
762
 
763
763
  @property
@@ -107,10 +107,12 @@ class GenericDataPoint[ParameterT: GenericParameterType, InputParameterT: Generi
107
107
  return set()
108
108
 
109
109
  converted_value = self._convert_value(value=prepared_value)
110
+ # if collector is set, then add value to collector
110
111
  if collector:
111
112
  collector.add_data_point(self, value=converted_value, collector_order=collector_order)
112
113
  return set()
113
114
 
115
+ # if collector is not set, then send value directly
114
116
  if self._validate_state_change and not self.is_state_change(value=converted_value):
115
117
  return set()
116
118
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic
3
- Version: 2025.9.8
3
+ Version: 2025.10.0
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>
@@ -379,15 +379,26 @@ async def test_cerfthermostat_with_profiles(
379
379
  await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "CONTROL_MODE", _ModeHmIP.MANU.value)
380
380
  assert climate.mode == ClimateMode.HEAT
381
381
 
382
+ await climate.set_temperature(13.0)
383
+ assert mock_client.method_calls[-1] == call.set_value(
384
+ channel_address="VCU0000341:2",
385
+ paramset_key=ParamsetKey.VALUES,
386
+ parameter="SET_TEMPERATURE",
387
+ value=13.0,
388
+ wait_for_callback=WAIT_FOR_CALLBACK,
389
+ )
390
+ assert climate._old_manu_setpoint == 13.0
391
+
382
392
  await climate.set_mode(ClimateMode.OFF)
383
393
  assert mock_client.method_calls[-1] == call.put_paramset(
384
394
  channel_address="VCU0000341:2",
385
395
  paramset_key_or_link_address=ParamsetKey.VALUES,
386
- values={"MANU_MODE": 12.0, "SET_TEMPERATURE": 4.5},
396
+ values={"MANU_MODE": 13.0, "SET_TEMPERATURE": 4.5},
387
397
  wait_for_callback=WAIT_FOR_CALLBACK,
388
398
  )
389
399
 
390
400
  assert climate.mode == ClimateMode.OFF
401
+ assert climate._old_manu_setpoint == 13.0
391
402
 
392
403
  await climate.set_mode(ClimateMode.AUTO)
393
404
  assert mock_client.method_calls[-1] == call.set_value(
@@ -397,10 +408,35 @@ async def test_cerfthermostat_with_profiles(
397
408
  value=True,
398
409
  wait_for_callback=WAIT_FOR_CALLBACK,
399
410
  )
400
- await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "CONTROL_MODE", 0)
411
+ await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "CONTROL_MODE", _ModeHmIP.AUTO.value)
401
412
  await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "SET_TEMPERATURE", 24.0)
402
413
  assert climate.mode == ClimateMode.AUTO
414
+ assert climate._old_manu_setpoint == 13.0
415
+ assert climate.target_temperature == 24.0
416
+ await climate.set_mode(ClimateMode.HEAT)
417
+ assert mock_client.method_calls[-1] == call.set_value(
418
+ channel_address="VCU0000341:2",
419
+ paramset_key=ParamsetKey.VALUES,
420
+ parameter="MANU_MODE",
421
+ value=climate._temperature_for_heat_mode,
422
+ wait_for_callback=WAIT_FOR_CALLBACK,
423
+ )
424
+ await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "CONTROL_MODE", _ModeHmIP.MANU.value)
425
+ await central.data_point_event(
426
+ const.INTERFACE_ID, "VCU0000341:2", "SET_TEMPERATURE", climate._temperature_for_heat_mode
427
+ )
428
+ assert climate.mode == ClimateMode.HEAT
403
429
 
430
+ await climate.set_mode(ClimateMode.AUTO)
431
+ assert mock_client.method_calls[-1] == call.set_value(
432
+ channel_address="VCU0000341:2",
433
+ paramset_key=ParamsetKey.VALUES,
434
+ parameter="AUTO_MODE",
435
+ value=True,
436
+ wait_for_callback=WAIT_FOR_CALLBACK,
437
+ )
438
+ await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "CONTROL_MODE", _ModeHmIP.AUTO.value)
439
+ await central.data_point_event(const.INTERFACE_ID, "VCU0000341:2", "SET_TEMPERATURE", 24.0)
404
440
  assert climate.profile == ClimateProfile.WEEK_PROGRAM_1
405
441
  assert climate.profiles == (
406
442
  ClimateProfile.BOOST,
@@ -568,7 +604,7 @@ async def test_ceipthermostat(
568
604
  assert climate.activity == ClimateActivity.HEAT
569
605
  await central.data_point_event(const.INTERFACE_ID, "VCU1769958:9", "STATE", 0)
570
606
  assert climate.activity == ClimateActivity.IDLE
571
-
607
+ assert climate._old_manu_setpoint is None
572
608
  assert climate.current_humidity is None
573
609
  await central.data_point_event(const.INTERFACE_ID, "VCU1769958:1", "HUMIDITY", 75)
574
610
  assert climate.current_humidity == 75
@@ -609,8 +645,18 @@ async def test_ceipthermostat(
609
645
  values={"CONTROL_MODE": 1, "SET_POINT_TEMPERATURE": 5.0},
610
646
  wait_for_callback=WAIT_FOR_CALLBACK,
611
647
  )
648
+ await climate.set_temperature(19.5)
649
+ assert mock_client.method_calls[-1] == call.set_value(
650
+ channel_address="VCU1769958:1",
651
+ paramset_key=ParamsetKey.VALUES,
652
+ parameter="SET_POINT_TEMPERATURE",
653
+ value=19.5,
654
+ wait_for_callback=WAIT_FOR_CALLBACK,
655
+ )
656
+ await central.data_point_event(const.INTERFACE_ID, "VCU1769958:1", "SET_POINT_TEMPERATURE", 19.5)
612
657
  await central.data_point_event(const.INTERFACE_ID, "VCU1769958:1", "SET_POINT_MODE", _ModeHmIP.MANU.value)
613
658
  assert climate.mode == ClimateMode.HEAT
659
+ assert climate._old_manu_setpoint == 19.5
614
660
 
615
661
  assert climate.profile == ClimateProfile.NONE
616
662
  assert climate.profiles == (
@@ -648,13 +694,26 @@ async def test_ceipthermostat(
648
694
  "week_program_5",
649
695
  "week_program_6",
650
696
  )
697
+
698
+ await climate.set_mode(ClimateMode.HEAT)
699
+ assert mock_client.method_calls[-1] == call.put_paramset(
700
+ channel_address="VCU1769958:1",
701
+ paramset_key_or_link_address=ParamsetKey.VALUES,
702
+ values={"BOOST_MODE": False, "CONTROL_MODE": 1, "SET_POINT_TEMPERATURE": climate._temperature_for_heat_mode},
703
+ wait_for_callback=None,
704
+ )
705
+
706
+ await central.data_point_event(const.INTERFACE_ID, "VCU1769958:1", "SET_POINT_TEMPERATURE", 19.5)
707
+ await central.data_point_event(const.INTERFACE_ID, "VCU1769958:1", "SET_POINT_MODE", _ModeHmIP.MANU.value)
708
+ assert climate.mode == ClimateMode.HEAT
709
+ assert climate.target_temperature == 19.5
710
+
651
711
  await climate.set_profile(ClimateProfile.NONE)
652
- assert mock_client.method_calls[-1] == call.set_value(
712
+ assert mock_client.method_calls[-1] == call.put_paramset(
653
713
  channel_address="VCU1769958:1",
654
- paramset_key=ParamsetKey.VALUES,
655
- parameter="BOOST_MODE",
656
- value=False,
657
- wait_for_callback=WAIT_FOR_CALLBACK,
714
+ paramset_key_or_link_address=ParamsetKey.VALUES,
715
+ values={"BOOST_MODE": False, "CONTROL_MODE": 1, "SET_POINT_TEMPERATURE": 19.5},
716
+ wait_for_callback=None,
658
717
  )
659
718
  await central.data_point_event(const.INTERFACE_ID, "VCU1769958:1", "SET_POINT_MODE", _ModeHmIP.AWAY.value)
660
719
  assert climate.profile == ClimateProfile.AWAY
@@ -716,7 +775,7 @@ async def test_ceipthermostat(
716
775
  await central.data_point_event(const.INTERFACE_ID, "VCU1769958:1", "SET_POINT_TEMPERATURE", 12.0)
717
776
  call_count = len(mock_client.method_calls)
718
777
  await climate.set_temperature(12.0)
719
- assert call_count == len(mock_client.method_calls)
778
+ assert call_count + 1 == len(mock_client.method_calls)
720
779
 
721
780
  await climate.set_mode(ClimateMode.AUTO)
722
781
  call_count = len(mock_client.method_calls)