aiohomematic 2025.10.1__py3-none-any.whl → 2025.10.2__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 (56) hide show
  1. aiohomematic/async_support.py +7 -7
  2. aiohomematic/caches/dynamic.py +31 -26
  3. aiohomematic/caches/persistent.py +34 -32
  4. aiohomematic/caches/visibility.py +19 -7
  5. aiohomematic/central/__init__.py +87 -74
  6. aiohomematic/central/decorators.py +2 -2
  7. aiohomematic/central/xml_rpc_server.py +27 -24
  8. aiohomematic/client/__init__.py +72 -56
  9. aiohomematic/client/_rpc_errors.py +3 -3
  10. aiohomematic/client/json_rpc.py +33 -25
  11. aiohomematic/client/xml_rpc.py +14 -9
  12. aiohomematic/const.py +2 -1
  13. aiohomematic/converter.py +19 -19
  14. aiohomematic/exceptions.py +2 -1
  15. aiohomematic/model/__init__.py +4 -3
  16. aiohomematic/model/calculated/__init__.py +1 -1
  17. aiohomematic/model/calculated/climate.py +9 -9
  18. aiohomematic/model/calculated/data_point.py +13 -7
  19. aiohomematic/model/calculated/operating_voltage_level.py +2 -2
  20. aiohomematic/model/calculated/support.py +7 -7
  21. aiohomematic/model/custom/__init__.py +3 -3
  22. aiohomematic/model/custom/climate.py +57 -34
  23. aiohomematic/model/custom/cover.py +32 -18
  24. aiohomematic/model/custom/data_point.py +9 -7
  25. aiohomematic/model/custom/definition.py +23 -17
  26. aiohomematic/model/custom/light.py +52 -23
  27. aiohomematic/model/custom/lock.py +16 -12
  28. aiohomematic/model/custom/siren.py +6 -3
  29. aiohomematic/model/custom/switch.py +3 -2
  30. aiohomematic/model/custom/valve.py +3 -2
  31. aiohomematic/model/data_point.py +62 -49
  32. aiohomematic/model/device.py +48 -42
  33. aiohomematic/model/event.py +6 -5
  34. aiohomematic/model/generic/__init__.py +6 -4
  35. aiohomematic/model/generic/action.py +1 -1
  36. aiohomematic/model/generic/data_point.py +7 -5
  37. aiohomematic/model/generic/number.py +3 -3
  38. aiohomematic/model/generic/select.py +1 -1
  39. aiohomematic/model/generic/sensor.py +2 -2
  40. aiohomematic/model/generic/switch.py +3 -3
  41. aiohomematic/model/hub/__init__.py +17 -16
  42. aiohomematic/model/hub/data_point.py +12 -7
  43. aiohomematic/model/hub/number.py +3 -3
  44. aiohomematic/model/hub/select.py +3 -3
  45. aiohomematic/model/hub/text.py +2 -2
  46. aiohomematic/model/support.py +8 -7
  47. aiohomematic/model/update.py +6 -6
  48. aiohomematic/support.py +44 -38
  49. aiohomematic/validator.py +6 -6
  50. {aiohomematic-2025.10.1.dist-info → aiohomematic-2025.10.2.dist-info}/METADATA +1 -1
  51. aiohomematic-2025.10.2.dist-info/RECORD +78 -0
  52. aiohomematic_support/client_local.py +19 -12
  53. aiohomematic-2025.10.1.dist-info/RECORD +0 -78
  54. {aiohomematic-2025.10.1.dist-info → aiohomematic-2025.10.2.dist-info}/WHEEL +0 -0
  55. {aiohomematic-2025.10.1.dist-info → aiohomematic-2025.10.2.dist-info}/licenses/LICENSE +0 -0
  56. {aiohomematic-2025.10.1.dist-info → aiohomematic-2025.10.2.dist-info}/top_level.txt +0 -0
@@ -44,6 +44,7 @@ from aiohomematic.const import (
44
44
  DP_KEY_VALUE,
45
45
  INIT_DATETIME,
46
46
  KEY_CHANNEL_OPERATION_MODE_VISIBILITY,
47
+ KWARGS_ARG_CUSTOM_ID,
47
48
  KWARGS_ARG_DATA_POINT,
48
49
  NO_CACHE_ENTRY,
49
50
  WAIT_FOR_CALLBACK,
@@ -142,7 +143,7 @@ class CallbackDataPoint(ABC, LogContextMixin):
142
143
  "_custom_id",
143
144
  "_data_point_updated_callbacks",
144
145
  "_device_removed_callbacks",
145
- "_fired_at",
146
+ "_fired_event_at",
146
147
  "_modified_at",
147
148
  "_path_data",
148
149
  "_refreshed_at",
@@ -154,7 +155,7 @@ class CallbackDataPoint(ABC, LogContextMixin):
154
155
 
155
156
  _category = DataPointCategory.UNDEFINED
156
157
 
157
- def __init__(self, central: hmcu.CentralUnit, unique_id: str) -> None:
158
+ def __init__(self, *, central: hmcu.CentralUnit, unique_id: str) -> None:
158
159
  """Init the callback data_point."""
159
160
  self._central: Final = central
160
161
  self._unique_id: Final = unique_id
@@ -162,7 +163,7 @@ class CallbackDataPoint(ABC, LogContextMixin):
162
163
  self._device_removed_callbacks: list[Callable] = []
163
164
  self._custom_id: str | None = None
164
165
  self._path_data = self._get_path_data()
165
- self._fired_at: datetime = INIT_DATETIME
166
+ self._fired_event_at: datetime = INIT_DATETIME
166
167
  self._modified_at: datetime = INIT_DATETIME
167
168
  self._refreshed_at: datetime = INIT_DATETIME
168
169
  self._signature: Final = self._get_signature()
@@ -190,16 +191,16 @@ class CallbackDataPoint(ABC, LogContextMixin):
190
191
  return self._custom_id
191
192
 
192
193
  @property
193
- def fired_at(self) -> datetime:
194
- """Return the data point updated fired at."""
195
- return self._fired_at
194
+ def fired_event_at(self) -> datetime:
195
+ """Return the data point updated fired an event at."""
196
+ return self._fired_event_at
196
197
 
197
198
  @state_property
198
- def fired_recently(self) -> bool:
199
- """Return the data point fired within 500 milliseconds."""
200
- if self._fired_at == INIT_DATETIME:
199
+ def fired_event_recently(self) -> bool:
200
+ """Return the data point fired an event within 500 milliseconds."""
201
+ if self._fired_event_at == INIT_DATETIME:
201
202
  return False
202
- return (datetime.now() - self._fired_at).total_seconds() < 0.5
203
+ return (datetime.now() - self._fired_event_at).total_seconds() < 0.5
203
204
 
204
205
  @classmethod
205
206
  def default_category(cls) -> DataPointCategory:
@@ -305,11 +306,11 @@ class CallbackDataPoint(ABC, LogContextMixin):
305
306
  """Return all service methods."""
306
307
  return tuple(self.service_methods.keys())
307
308
 
308
- def register_internal_data_point_updated_callback(self, cb: Callable) -> CALLBACK_TYPE:
309
+ def register_internal_data_point_updated_callback(self, *, cb: Callable) -> CALLBACK_TYPE:
309
310
  """Register internal data_point updated callback."""
310
311
  return self.register_data_point_updated_callback(cb=cb, custom_id=DEFAULT_CUSTOM_ID)
311
312
 
312
- def register_data_point_updated_callback(self, cb: Callable, custom_id: str) -> CALLBACK_TYPE:
313
+ def register_data_point_updated_callback(self, *, cb: Callable, custom_id: str) -> CALLBACK_TYPE:
313
314
  """Register data_point updated callback."""
314
315
  if custom_id != DEFAULT_CUSTOM_ID:
315
316
  if self._custom_id is not None and self._custom_id != custom_id:
@@ -336,45 +337,46 @@ class CallbackDataPoint(ABC, LogContextMixin):
336
337
  def _get_signature(self) -> str:
337
338
  """Return the signature of the data_point."""
338
339
 
339
- def _unregister_data_point_updated_callback(self, cb: Callable, custom_id: str) -> None:
340
+ def _unregister_data_point_updated_callback(self, *, cb: Callable, custom_id: str) -> None:
340
341
  """Unregister data_point updated callback."""
341
342
  if cb in self._data_point_updated_callbacks:
342
343
  del self._data_point_updated_callbacks[cb]
343
344
  if self.custom_id == custom_id:
344
345
  self._custom_id = None
345
346
 
346
- def register_device_removed_callback(self, cb: Callable) -> CALLBACK_TYPE:
347
+ def register_device_removed_callback(self, *, cb: Callable) -> CALLBACK_TYPE:
347
348
  """Register the device removed callback."""
348
349
  if callable(cb) and cb not in self._device_removed_callbacks:
349
350
  self._device_removed_callbacks.append(cb)
350
351
  return partial(self._unregister_device_removed_callback, cb=cb)
351
352
  return None
352
353
 
353
- def _unregister_device_removed_callback(self, cb: Callable) -> None:
354
+ def _unregister_device_removed_callback(self, *, cb: Callable) -> None:
354
355
  """Unregister the device removed callback."""
355
356
  if cb in self._device_removed_callbacks:
356
357
  self._device_removed_callbacks.remove(cb)
357
358
 
358
359
  @loop_check
359
- def fire_data_point_updated_callback(self, *args: Any, **kwargs: Any) -> None:
360
+ def fire_data_point_updated_callback(self, **kwargs: Any) -> None:
360
361
  """Do what is needed when the value of the data_point has been updated/refreshed."""
361
362
  if not self._should_fire_data_point_updated_callback:
362
363
  return
363
- self._fired_at = datetime.now()
364
- # Add the data_point reference once to kwargs to avoid per-callback writes.
365
- kwargs[KWARGS_ARG_DATA_POINT] = self
366
- for callback_handler in self._data_point_updated_callbacks:
364
+ self._fired_event_at = datetime.now()
365
+ for callback_handler, custom_id in self._data_point_updated_callbacks.items():
367
366
  try:
368
- callback_handler(*args, **kwargs)
367
+ # Add the data_point reference once to kwargs to avoid per-callback writes.
368
+ kwargs[KWARGS_ARG_DATA_POINT] = self
369
+ kwargs[KWARGS_ARG_CUSTOM_ID] = custom_id
370
+ callback_handler(**kwargs)
369
371
  except Exception as exc:
370
372
  _LOGGER.warning("FIRE_DATA_POINT_UPDATED_EVENT failed: %s", extract_exc_args(exc=exc))
371
373
 
372
374
  @loop_check
373
- def fire_device_removed_callback(self, *args: Any) -> None:
375
+ def fire_device_removed_callback(self) -> None:
374
376
  """Do what is needed when the data_point has been removed."""
375
377
  for callback_handler in self._device_removed_callbacks:
376
378
  try:
377
- callback_handler(*args)
379
+ callback_handler()
378
380
  except Exception as exc:
379
381
  _LOGGER.warning("FIRE_DEVICE_REMOVED_EVENT failed: %s", extract_exc_args(exc=exc))
380
382
 
@@ -383,21 +385,21 @@ class CallbackDataPoint(ABC, LogContextMixin):
383
385
  """Check if a data point has been updated or refreshed."""
384
386
  return True
385
387
 
386
- def _set_modified_at(self, modified_at: datetime) -> None:
388
+ def _set_modified_at(self, *, modified_at: datetime) -> None:
387
389
  """Set modified_at to current datetime."""
388
390
  self._modified_at = modified_at
389
391
  self._set_refreshed_at(refreshed_at=modified_at)
390
392
 
391
- def _set_refreshed_at(self, refreshed_at: datetime) -> None:
393
+ def _set_refreshed_at(self, *, refreshed_at: datetime) -> None:
392
394
  """Set refreshed_at to current datetime."""
393
395
  self._refreshed_at = refreshed_at
394
396
 
395
- def _set_temporary_modified_at(self, modified_at: datetime) -> None:
397
+ def _set_temporary_modified_at(self, *, modified_at: datetime) -> None:
396
398
  """Set temporary_modified_at to current datetime."""
397
399
  self._temporary_modified_at = modified_at
398
400
  self._set_temporary_refreshed_at(refreshed_at=modified_at)
399
401
 
400
- def _set_temporary_refreshed_at(self, refreshed_at: datetime) -> None:
402
+ def _set_temporary_refreshed_at(self, *, refreshed_at: datetime) -> None:
401
403
  """Set temporary_refreshed_at to current datetime."""
402
404
  self._temporary_refreshed_at = refreshed_at
403
405
 
@@ -426,6 +428,7 @@ class BaseDataPoint(CallbackDataPoint, PayloadMixin):
426
428
 
427
429
  def __init__(
428
430
  self,
431
+ *,
429
432
  channel: hmd.Channel,
430
433
  unique_id: str,
431
434
  is_in_multiple_channels: bool,
@@ -507,7 +510,7 @@ class BaseDataPoint(CallbackDataPoint, PayloadMixin):
507
510
  """Return the data_point usage."""
508
511
  return self._get_data_point_usage()
509
512
 
510
- def force_usage(self, forced_usage: DataPointUsage) -> None:
513
+ def force_usage(self, *, forced_usage: DataPointUsage) -> None:
511
514
  """Set the data_point usage."""
512
515
  self._forced_usage = forced_usage
513
516
 
@@ -525,7 +528,7 @@ class BaseDataPoint(CallbackDataPoint, PayloadMixin):
525
528
  return on_time
526
529
 
527
530
  @abstractmethod
528
- async def load_data_point_value(self, call_source: CallSource, direct_call: bool = False) -> None:
531
+ async def load_data_point_value(self, *, call_source: CallSource, direct_call: bool = False) -> None:
529
532
  """Init the data_point data."""
530
533
 
531
534
  @abstractmethod
@@ -536,7 +539,7 @@ class BaseDataPoint(CallbackDataPoint, PayloadMixin):
536
539
  def _get_data_point_usage(self) -> DataPointUsage:
537
540
  """Generate the usage for the data_point."""
538
541
 
539
- def set_timer_on_time(self, on_time: float) -> None:
542
+ def set_timer_on_time(self, *, on_time: float) -> None:
540
543
  """Set the on_time."""
541
544
  self._timer_on_time = on_time
542
545
  self._timer_on_time_end = INIT_DATETIME
@@ -580,6 +583,7 @@ class BaseParameterDataPoint[
580
583
 
581
584
  def __init__(
582
585
  self,
586
+ *,
583
587
  channel: hmd.Channel,
584
588
  paramset_key: ParamsetKey,
585
589
  parameter: str,
@@ -618,13 +622,13 @@ class BaseParameterDataPoint[
618
622
  self._is_forced_sensor: bool = False
619
623
  self._assign_parameter_data(parameter_data=parameter_data)
620
624
 
621
- def _assign_parameter_data(self, parameter_data: ParameterData) -> None:
625
+ def _assign_parameter_data(self, *, parameter_data: ParameterData) -> None:
622
626
  """Assign parameter data to instance variables."""
623
627
  self._type: ParameterType = ParameterType(parameter_data["TYPE"])
624
628
  self._values = tuple(parameter_data["VALUE_LIST"]) if parameter_data.get("VALUE_LIST") else None
625
- self._max: ParameterT = self._convert_value(parameter_data["MAX"])
626
- self._min: ParameterT = self._convert_value(parameter_data["MIN"])
627
- self._default: ParameterT = self._convert_value(parameter_data.get("DEFAULT")) or self._min
629
+ self._max: ParameterT = self._convert_value(value=parameter_data["MAX"])
630
+ self._min: ParameterT = self._convert_value(value=parameter_data["MIN"])
631
+ self._default: ParameterT = self._convert_value(value=parameter_data.get("DEFAULT")) or self._min
628
632
  flags: int = parameter_data["FLAGS"]
629
633
  self._visible: bool = flags & Flag.VISIBLE == Flag.VISIBLE
630
634
  self._service: bool = flags & Flag.SERVICE == Flag.SERVICE
@@ -832,7 +836,7 @@ class BaseParameterDataPoint[
832
836
  )
833
837
  self._is_forced_sensor = True
834
838
 
835
- def _cleanup_unit(self, raw_unit: str | None) -> str | None:
839
+ def _cleanup_unit(self, *, raw_unit: str | None) -> str | None:
836
840
  """Replace given unit."""
837
841
  if new_unit := _FIX_UNIT_BY_PARAM.get(self._parameter):
838
842
  return new_unit
@@ -843,7 +847,7 @@ class BaseParameterDataPoint[
843
847
  return fix
844
848
  return raw_unit
845
849
 
846
- def _get_multiplier(self, raw_unit: str | None) -> float:
850
+ def _get_multiplier(self, *, raw_unit: str | None) -> float:
847
851
  """Replace given unit."""
848
852
  if not raw_unit:
849
853
  return DEFAULT_MULTIPLIER
@@ -856,10 +860,10 @@ class BaseParameterDataPoint[
856
860
  return f"{self._category}/{self._channel.device.model}/{self._parameter}"
857
861
 
858
862
  @abstractmethod
859
- async def event(self, value: Any, received_at: datetime) -> None:
863
+ async def event(self, *, value: Any, received_at: datetime) -> None:
860
864
  """Handle event for which this handler has subscribed."""
861
865
 
862
- async def load_data_point_value(self, call_source: CallSource, direct_call: bool = False) -> None:
866
+ async def load_data_point_value(self, *, call_source: CallSource, direct_call: bool = False) -> None:
863
867
  """Init the data_point data."""
864
868
  if (self._ignore_on_initial_load or self._channel.device.ignore_on_initial_load) and call_source in (
865
869
  CallSource.HM_INIT,
@@ -883,7 +887,7 @@ class BaseParameterDataPoint[
883
887
  write_at=datetime.now(),
884
888
  )
885
889
 
886
- def write_value(self, value: Any, write_at: datetime) -> tuple[ParameterT, ParameterT]:
890
+ def write_value(self, *, value: Any, write_at: datetime) -> tuple[ParameterT, ParameterT]:
887
891
  """Update value of the data_point."""
888
892
  self._reset_temporary_value()
889
893
 
@@ -894,7 +898,7 @@ class BaseParameterDataPoint[
894
898
  self.fire_data_point_updated_callback()
895
899
  return (old_value, None) # type: ignore[return-value]
896
900
 
897
- new_value = self._convert_value(value)
901
+ new_value = self._convert_value(value=value)
898
902
  if old_value == new_value:
899
903
  self._set_refreshed_at(refreshed_at=write_at)
900
904
  else:
@@ -905,11 +909,11 @@ class BaseParameterDataPoint[
905
909
  self.fire_data_point_updated_callback()
906
910
  return (old_value, new_value)
907
911
 
908
- def write_temporary_value(self, value: Any, write_at: datetime) -> None:
912
+ def write_temporary_value(self, *, value: Any, write_at: datetime) -> None:
909
913
  """Update the temporary value of the data_point."""
910
914
  self._reset_temporary_value()
911
915
 
912
- temp_value = self._convert_value(value)
916
+ temp_value = self._convert_value(value=value)
913
917
  if self._value == temp_value:
914
918
  self._set_temporary_refreshed_at(refreshed_at=write_at)
915
919
  else:
@@ -928,7 +932,7 @@ class BaseParameterDataPoint[
928
932
  ):
929
933
  self._assign_parameter_data(parameter_data=parameter_data)
930
934
 
931
- def _convert_value(self, value: Any) -> ParameterT:
935
+ def _convert_value(self, *, value: Any) -> ParameterT:
932
936
  """Convert to value to ParameterT."""
933
937
  if value is None:
934
938
  return None # type: ignore[return-value]
@@ -963,7 +967,7 @@ class BaseParameterDataPoint[
963
967
  self._temporary_value = None # type: ignore[assignment]
964
968
  self._reset_temporary_timestamps()
965
969
 
966
- def get_event_data(self, value: Any = None) -> dict[EventKey, Any]:
970
+ def get_event_data(self, *, value: Any = None) -> dict[EventKey, Any]:
967
971
  """Get the event_data."""
968
972
  event_data = {
969
973
  EventKey.ADDRESS: self._device.address,
@@ -986,7 +990,7 @@ class CallParameterCollector:
986
990
  "_paramsets",
987
991
  )
988
992
 
989
- def __init__(self, client: hmcl.Client) -> None:
993
+ def __init__(self, *, client: hmcl.Client) -> None:
990
994
  """Init the generator."""
991
995
  self._client: Final = client
992
996
  self._central: Final = client.central
@@ -1010,7 +1014,7 @@ class CallParameterCollector:
1010
1014
  value
1011
1015
  )
1012
1016
 
1013
- async def send_data(self, wait_for_callback: int | None) -> set[DP_KEY_VALUE]:
1017
+ async def send_data(self, *, wait_for_callback: int | None) -> set[DP_KEY_VALUE]:
1014
1018
  """Send data to the backend."""
1015
1019
  dpk_values: set[DP_KEY_VALUE] = set()
1016
1020
  for paramset_key, paramsets in self._paramsets.items():
@@ -1040,6 +1044,7 @@ class CallParameterCollector:
1040
1044
 
1041
1045
 
1042
1046
  def bind_collector(
1047
+ *,
1043
1048
  wait_for_callback: int | None = WAIT_FOR_CALLBACK,
1044
1049
  enabled: bool = True,
1045
1050
  log_level: int = logging.ERROR,
@@ -1052,7 +1057,12 @@ def bind_collector(
1052
1057
 
1053
1058
  def bind_decorator[CallableT: Callable[..., Any]](func: CallableT) -> CallableT:
1054
1059
  """Decorate function to automatically add collector if not set."""
1055
- argument_index = getfullargspec(func).args.index(_COLLECTOR_ARGUMENT_NAME)
1060
+ spec = getfullargspec(func)
1061
+ # Support both positional and keyword-only 'collector' parameters
1062
+ if _COLLECTOR_ARGUMENT_NAME in spec.args:
1063
+ argument_index: int | None = spec.args.index(_COLLECTOR_ARGUMENT_NAME)
1064
+ else:
1065
+ argument_index = None
1056
1066
 
1057
1067
  @wraps(func)
1058
1068
  async def bind_wrapper(*args: Any, **kwargs: Any) -> Any:
@@ -1067,8 +1077,10 @@ def bind_collector(
1067
1077
  IN_SERVICE_VAR.reset(token)
1068
1078
  return return_value
1069
1079
  try:
1070
- collector_exists = args[argument_index] is not None
1071
- except IndexError:
1080
+ collector_exists = (
1081
+ argument_index is not None and len(args) > argument_index and args[argument_index] is not None
1082
+ ) or kwargs.get(_COLLECTOR_ARGUMENT_NAME) is not None
1083
+ except Exception:
1072
1084
  collector_exists = kwargs.get(_COLLECTOR_ARGUMENT_NAME) is not None
1073
1085
 
1074
1086
  if collector_exists:
@@ -1127,6 +1139,7 @@ class NoneTypeDataPoint:
1127
1139
 
1128
1140
  async def send_value(
1129
1141
  self,
1142
+ *,
1130
1143
  value: Any,
1131
1144
  collector: CallParameterCollector | None = None,
1132
1145
  do_validate: bool = True,