aiohomematic 2025.10.0__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 +90 -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 +3 -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 +44 -20
  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.0.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.0.dist-info/RECORD +0 -78
  54. {aiohomematic-2025.10.0.dist-info → aiohomematic-2025.10.2.dist-info}/WHEEL +0 -0
  55. {aiohomematic-2025.10.0.dist-info → aiohomematic-2025.10.2.dist-info}/licenses/LICENSE +0 -0
  56. {aiohomematic-2025.10.0.dist-info → aiohomematic-2025.10.2.dist-info}/top_level.txt +0 -0
@@ -105,6 +105,7 @@ from aiohomematic.const import (
105
105
  DEFAULT_SYSVAR_MARKERS,
106
106
  DEFAULT_TLS,
107
107
  DEFAULT_UN_IGNORES,
108
+ DEFAULT_USE_GROUP_CHANNEL_FOR_COVER_STATE,
108
109
  DEFAULT_VERIFY_TLS,
109
110
  DEVICE_FIRMWARE_CHECK_INTERVAL,
110
111
  DEVICE_FIRMWARE_DELIVERING_CHECK_INTERVAL,
@@ -192,7 +193,7 @@ INTERFACE_EVENT_SCHEMA = vol.Schema(
192
193
  class CentralUnit(LogContextMixin, PayloadMixin):
193
194
  """Central unit that collects everything to handle communication from/to the backend."""
194
195
 
195
- def __init__(self, central_config: CentralConfig) -> None:
196
+ def __init__(self, *, central_config: CentralConfig) -> None:
196
197
  """Init the central unit."""
197
198
  self._state: CentralUnitState = CentralUnitState.NEW
198
199
  self._clients_started: bool = False
@@ -218,7 +219,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
218
219
  # {interface_id, client}
219
220
  self._clients: Final[dict[str, hmcl.Client]] = {}
220
221
  self._data_point_key_event_subscriptions: Final[
221
- dict[DataPointKey, list[Callable[[Any, datetime], Coroutine[Any, Any, None]]]]
222
+ dict[DataPointKey, list[Callable[..., Coroutine[Any, Any, None]]]]
222
223
  ] = {}
223
224
  self._data_point_path_event_subscriptions: Final[dict[str, DataPointKey]] = {}
224
225
  self._sysvar_data_point_event_subscriptions: Final[dict[str, Callable]] = {}
@@ -228,7 +229,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
228
229
  self._sysvar_data_points: Final[dict[str, GenericSysvarDataPoint]] = {}
229
230
  # {sysvar_name, program_button}
230
231
  self._program_data_points: Final[dict[str, ProgramDpType]] = {}
231
- # Signature: (name, *args)
232
+ # Signature: (system_event, new_data_points, new_channel_events, **kwargs)
232
233
  # e.g. DEVICES_CREATED, HUB_REFRESHED
233
234
  self._backend_system_callbacks: Final[set[Callable]] = set()
234
235
  # Signature: (interface_id, channel_address, parameter, value)
@@ -420,14 +421,14 @@ class CentralUnit(LogContextMixin, PayloadMixin):
420
421
  self._version = max(versions) if versions else None
421
422
  return self._version
422
423
 
423
- def add_sysvar_data_point(self, sysvar_data_point: GenericSysvarDataPoint) -> None:
424
+ def add_sysvar_data_point(self, *, sysvar_data_point: GenericSysvarDataPoint) -> None:
424
425
  """Add new program button."""
425
426
  if (vid := sysvar_data_point.vid) is not None:
426
427
  self._sysvar_data_points[vid] = sysvar_data_point
427
428
  if sysvar_data_point.state_path not in self._sysvar_data_point_event_subscriptions:
428
429
  self._sysvar_data_point_event_subscriptions[sysvar_data_point.state_path] = sysvar_data_point.event
429
430
 
430
- def remove_sysvar_data_point(self, vid: str) -> None:
431
+ def remove_sysvar_data_point(self, *, vid: str) -> None:
431
432
  """Remove a sysvar data_point."""
432
433
  if (sysvar_dp := self.get_sysvar_data_point(vid=vid)) is not None:
433
434
  sysvar_dp.fire_device_removed_callback()
@@ -435,18 +436,18 @@ class CentralUnit(LogContextMixin, PayloadMixin):
435
436
  if sysvar_dp.state_path in self._sysvar_data_point_event_subscriptions:
436
437
  del self._sysvar_data_point_event_subscriptions[sysvar_dp.state_path]
437
438
 
438
- def add_program_data_point(self, program_dp: ProgramDpType) -> None:
439
+ def add_program_data_point(self, *, program_dp: ProgramDpType) -> None:
439
440
  """Add new program button."""
440
441
  self._program_data_points[program_dp.pid] = program_dp
441
442
 
442
- def remove_program_button(self, pid: str) -> None:
443
+ def remove_program_button(self, *, pid: str) -> None:
443
444
  """Remove a program button."""
444
445
  if (program_dp := self.get_program_data_point(pid=pid)) is not None:
445
446
  program_dp.button.fire_device_removed_callback()
446
447
  program_dp.switch.fire_device_removed_callback()
447
448
  del self._program_data_points[pid]
448
449
 
449
- def identify_channel(self, text: str) -> Channel | None:
450
+ def identify_channel(self, *, text: str) -> Channel | None:
450
451
  """Identify channel within a text."""
451
452
  for device in self._devices.values():
452
453
  if channel := device.identify_channel(text=text):
@@ -454,7 +455,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
454
455
  return None
455
456
 
456
457
  async def save_caches(
457
- self, save_device_descriptions: bool = False, save_paramset_descriptions: bool = False
458
+ self, *, save_device_descriptions: bool = False, save_paramset_descriptions: bool = False
458
459
  ) -> None:
459
460
  """Save persistent caches."""
460
461
  if save_device_descriptions:
@@ -495,7 +496,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
495
496
  ):
496
497
  self._xml_rpc_server = xml_rpc_server
497
498
  self._listen_port = xml_rpc_server.listen_port
498
- self._xml_rpc_server.add_central(self)
499
+ self._xml_rpc_server.add_central(central=self)
499
500
  except OSError as oserr:
500
501
  self._state = CentralUnitState.STOPPED_BY_ERROR
501
502
  raise AioHomematicException(
@@ -572,7 +573,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
572
573
  _LOGGER.info("RESTART_CLIENTS: Central %s restarted clients", self.name)
573
574
 
574
575
  @inspector(re_raise=False)
575
- async def refresh_firmware_data(self, device_address: str | None = None) -> None:
576
+ async def refresh_firmware_data(self, *, device_address: str | None = None) -> None:
576
577
  """Refresh device firmware data."""
577
578
  if device_address and (device := self.get_device(address=device_address)) is not None and device.is_updatable:
578
579
  await self._refresh_device_descriptions(client=device.client, device_address=device_address)
@@ -585,7 +586,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
585
586
  device.refresh_firmware_data()
586
587
 
587
588
  @inspector(re_raise=False)
588
- async def refresh_firmware_data_by_state(self, device_firmware_states: tuple[DeviceFirmwareState, ...]) -> None:
589
+ async def refresh_firmware_data_by_state(self, *, device_firmware_states: tuple[DeviceFirmwareState, ...]) -> None:
589
590
  """Refresh device firmware data for processing devices."""
590
591
  for device in [
591
592
  device_in_state
@@ -594,7 +595,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
594
595
  ]:
595
596
  await self.refresh_firmware_data(device_address=device.address)
596
597
 
597
- async def _refresh_device_descriptions(self, client: hmcl.Client, device_address: str | None = None) -> None:
598
+ async def _refresh_device_descriptions(self, *, client: hmcl.Client, device_address: str | None = None) -> None:
598
599
  """Refresh device descriptions."""
599
600
  device_descriptions: tuple[DeviceDescription, ...] | None = None
600
601
  if (
@@ -687,7 +688,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
687
688
  _LOGGER.debug("CREATE_CLIENTS successful for %s", self.name)
688
689
  return True
689
690
 
690
- async def _create_client(self, interface_config: hmcl.InterfaceConfig) -> bool:
691
+ async def _create_client(self, *, interface_config: hmcl.InterfaceConfig) -> bool:
691
692
  """Create a client."""
692
693
  try:
693
694
  if client := await hmcl.create_client(
@@ -743,6 +744,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
743
744
  @loop_check
744
745
  def fire_interface_event(
745
746
  self,
747
+ *,
746
748
  interface_id: str,
747
749
  interface_event_type: InterfaceEventType,
748
750
  data: dict[str, Any],
@@ -760,7 +762,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
760
762
  event_data=cast(dict[EventKey, Any], INTERFACE_EVENT_SCHEMA(event_data)),
761
763
  )
762
764
 
763
- async def _identify_ip_addr(self, port: int) -> str:
765
+ async def _identify_ip_addr(self, *, port: int) -> str:
764
766
  ip_addr: str | None = None
765
767
  while ip_addr is None:
766
768
  try:
@@ -810,24 +812,24 @@ class CentralUnit(LogContextMixin, PayloadMixin):
810
812
  system_information = client.system_information
811
813
  return system_information
812
814
 
813
- def get_client(self, interface_id: str) -> hmcl.Client:
815
+ def get_client(self, *, interface_id: str) -> hmcl.Client:
814
816
  """Return a client by interface_id."""
815
817
  if not self.has_client(interface_id=interface_id):
816
818
  raise AioHomematicException(f"get_client: interface_id {interface_id} does not exist on {self.name}")
817
819
  return self._clients[interface_id]
818
820
 
819
- def get_channel(self, channel_address: str) -> Channel | None:
821
+ def get_channel(self, *, channel_address: str) -> Channel | None:
820
822
  """Return Homematic channel."""
821
823
  if device := self.get_device(address=channel_address):
822
824
  return device.get_channel(channel_address=channel_address)
823
825
  return None
824
826
 
825
- def get_device(self, address: str) -> Device | None:
827
+ def get_device(self, *, address: str) -> Device | None:
826
828
  """Return Homematic device."""
827
829
  d_address = get_device_address(address=address)
828
830
  return self._devices.get(d_address)
829
831
 
830
- def get_data_point_by_custom_id(self, custom_id: str) -> CallbackDataPoint | None:
832
+ def get_data_point_by_custom_id(self, *, custom_id: str) -> CallbackDataPoint | None:
831
833
  """Return Homematic data_point by custom_id."""
832
834
  for dp in self.get_data_points(registered=True):
833
835
  if dp.custom_id == custom_id:
@@ -836,6 +838,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
836
838
 
837
839
  def get_data_points(
838
840
  self,
841
+ *,
839
842
  category: DataPointCategory | None = None,
840
843
  interface: Interface | None = None,
841
844
  exclude_no_create: bool = True,
@@ -852,7 +855,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
852
855
  return tuple(all_data_points)
853
856
 
854
857
  def get_readable_generic_data_points(
855
- self, paramset_key: ParamsetKey | None = None, interface: Interface | None = None
858
+ self, *, paramset_key: ParamsetKey | None = None, interface: Interface | None = None
856
859
  ) -> tuple[GenericDataPoint, ...]:
857
860
  """Return the readable generic data points."""
858
861
  return tuple(
@@ -874,7 +877,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
874
877
  return client
875
878
 
876
879
  def get_hub_data_points(
877
- self, category: DataPointCategory | None = None, registered: bool | None = None
880
+ self, *, category: DataPointCategory | None = None, registered: bool | None = None
878
881
  ) -> tuple[GenericHubDataPoint, ...]:
879
882
  """Return the program data points."""
880
883
  return tuple(
@@ -883,7 +886,9 @@ class CentralUnit(LogContextMixin, PayloadMixin):
883
886
  if (category is None or he.category == category) and (registered is None or he.is_registered == registered)
884
887
  )
885
888
 
886
- def get_events(self, event_type: EventType, registered: bool | None = None) -> tuple[tuple[GenericEvent, ...], ...]:
889
+ def get_events(
890
+ self, *, event_type: EventType, registered: bool | None = None
891
+ ) -> tuple[tuple[GenericEvent, ...], ...]:
887
892
  """Return all channel event data points."""
888
893
  hm_channel_events: list[tuple[GenericEvent, ...]] = []
889
894
  for device in self.devices:
@@ -901,7 +906,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
901
906
  if cl.get_virtual_remote() is not None
902
907
  )
903
908
 
904
- def has_client(self, interface_id: str) -> bool:
909
+ def has_client(self, *, interface_id: str) -> bool:
905
910
  """Check if client exists in central."""
906
911
  return interface_id in self._clients
907
912
 
@@ -929,7 +934,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
929
934
  await self._data_cache.load()
930
935
  return True
931
936
 
932
- async def _create_devices(self, new_device_addresses: Mapping[str, set[str]]) -> None:
937
+ async def _create_devices(self, *, new_device_addresses: Mapping[str, set[str]]) -> None:
933
938
  """Trigger creation of the objects that expose the functionality."""
934
939
  if not self._clients:
935
940
  raise AioHomematicException(
@@ -985,7 +990,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
985
990
  new_channel_events=new_channel_events,
986
991
  )
987
992
 
988
- async def delete_device(self, interface_id: str, device_address: str) -> None:
993
+ async def delete_device(self, *, interface_id: str, device_address: str) -> None:
989
994
  """Delete devices from central."""
990
995
  _LOGGER.debug(
991
996
  "DELETE_DEVICE: interface_id = %s, device_address = %s",
@@ -999,7 +1004,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
999
1004
  await self.delete_devices(interface_id=interface_id, addresses=[device_address, *list(device.channels.keys())])
1000
1005
 
1001
1006
  @callback_backend_system(system_event=BackendSystemEvent.DELETE_DEVICES)
1002
- async def delete_devices(self, interface_id: str, addresses: tuple[str, ...]) -> None:
1007
+ async def delete_devices(self, *, interface_id: str, addresses: tuple[str, ...]) -> None:
1003
1008
  """Delete devices from central."""
1004
1009
  _LOGGER.debug(
1005
1010
  "DELETE_DEVICES: interface_id = %s, addresses = %s",
@@ -1012,12 +1017,12 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1012
1017
  await self.save_caches()
1013
1018
 
1014
1019
  @callback_backend_system(system_event=BackendSystemEvent.NEW_DEVICES)
1015
- async def add_new_devices(self, interface_id: str, device_descriptions: tuple[DeviceDescription, ...]) -> None:
1020
+ async def add_new_devices(self, *, interface_id: str, device_descriptions: tuple[DeviceDescription, ...]) -> None:
1016
1021
  """Add new devices to central unit."""
1017
1022
  await self._add_new_devices(interface_id=interface_id, device_descriptions=device_descriptions)
1018
1023
 
1019
1024
  @inspector(measure_performance=True)
1020
- async def _add_new_devices(self, interface_id: str, device_descriptions: tuple[DeviceDescription, ...]) -> None:
1025
+ async def _add_new_devices(self, *, interface_id: str, device_descriptions: tuple[DeviceDescription, ...]) -> None:
1021
1026
  """Add new devices to central unit."""
1022
1027
  if not device_descriptions:
1023
1028
  _LOGGER.debug(
@@ -1103,7 +1108,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1103
1108
  return new_device_addresses
1104
1109
 
1105
1110
  @callback_event
1106
- async def data_point_event(self, interface_id: str, channel_address: str, parameter: str, value: Any) -> None:
1111
+ async def data_point_event(self, *, interface_id: str, channel_address: str, parameter: str, value: Any) -> None:
1107
1112
  """If a device emits some sort event, we will handle it here."""
1108
1113
  _LOGGER_EVENT.debug(
1109
1114
  "EVENT: interface_id = %s, channel_address = %s, parameter = %s, value = %s",
@@ -1142,7 +1147,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1142
1147
  received_at = datetime.now()
1143
1148
  for callback_handler in self._data_point_key_event_subscriptions[dpk]:
1144
1149
  if callable(callback_handler):
1145
- await callback_handler(value, received_at)
1150
+ await callback_handler(value=value, received_at=received_at)
1146
1151
  except RuntimeError as rterr: # pragma: no cover
1147
1152
  _LOGGER_EVENT.debug(
1148
1153
  "EVENT: RuntimeError [%s]. Failed to call callback for: %s, %s, %s",
@@ -1160,7 +1165,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1160
1165
  extract_exc_args(exc=exc),
1161
1166
  )
1162
1167
 
1163
- def data_point_path_event(self, state_path: str, value: str) -> None:
1168
+ def data_point_path_event(self, *, state_path: str, value: str) -> None:
1164
1169
  """If a device emits some sort event, we will handle it here."""
1165
1170
  _LOGGER_EVENT.debug(
1166
1171
  "DATA_POINT_PATH_EVENT: topic = %s, payload = %s",
@@ -1170,7 +1175,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1170
1175
 
1171
1176
  if (dpk := self._data_point_path_event_subscriptions.get(state_path)) is not None:
1172
1177
  self._looper.create_task(
1173
- self.data_point_event(
1178
+ target=self.data_point_event(
1174
1179
  interface_id=dpk.interface_id,
1175
1180
  channel_address=dpk.channel_address,
1176
1181
  parameter=dpk.parameter,
@@ -1179,7 +1184,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1179
1184
  name=f"device-data-point-event-{dpk.interface_id}-{dpk.channel_address}-{dpk.parameter}",
1180
1185
  )
1181
1186
 
1182
- def sysvar_data_point_path_event(self, state_path: str, value: str) -> None:
1187
+ def sysvar_data_point_path_event(self, *, state_path: str, value: str) -> None:
1183
1188
  """If a device emits some sort event, we will handle it here."""
1184
1189
  _LOGGER_EVENT.debug(
1185
1190
  "SYSVAR_DATA_POINT_PATH_EVENT: topic = %s, payload = %s",
@@ -1193,7 +1198,8 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1193
1198
  if callable(callback_handler):
1194
1199
  received_at = datetime.now()
1195
1200
  self._looper.create_task(
1196
- callback_handler(value, received_at), name=f"sysvar-data-point-event-{state_path}"
1201
+ target=callback_handler(value=value, received_at=received_at),
1202
+ name=f"sysvar-data-point-event-{state_path}",
1197
1203
  )
1198
1204
  except RuntimeError as rterr: # pragma: no cover
1199
1205
  _LOGGER_EVENT.debug(
@@ -1209,13 +1215,13 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1209
1215
  )
1210
1216
 
1211
1217
  @callback_backend_system(system_event=BackendSystemEvent.LIST_DEVICES)
1212
- def list_devices(self, interface_id: str) -> list[DeviceDescription]:
1218
+ def list_devices(self, *, interface_id: str) -> list[DeviceDescription]:
1213
1219
  """Return already existing devices to the backend."""
1214
1220
  result = self._device_descriptions.get_raw_device_descriptions(interface_id=interface_id)
1215
1221
  _LOGGER.debug("LIST_DEVICES: interface_id = %s, channel_count = %i", interface_id, len(result))
1216
1222
  return result
1217
1223
 
1218
- def add_event_subscription(self, data_point: BaseParameterDataPoint) -> None:
1224
+ def add_event_subscription(self, *, data_point: BaseParameterDataPoint) -> None:
1219
1225
  """Add data_point to central event subscription."""
1220
1226
  if isinstance(data_point, GenericDataPoint | GenericEvent) and (
1221
1227
  data_point.is_readable or data_point.supports_events
@@ -1241,7 +1247,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1241
1247
  for device in self.devices:
1242
1248
  await device.remove_central_links()
1243
1249
 
1244
- def remove_device(self, device: Device) -> None:
1250
+ def remove_device(self, *, device: Device) -> None:
1245
1251
  """Remove device to central collections."""
1246
1252
  if device.address not in self._devices:
1247
1253
  _LOGGER.debug(
@@ -1256,7 +1262,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1256
1262
  self._device_details.remove_device(device=device)
1257
1263
  del self._devices[device.address]
1258
1264
 
1259
- def remove_event_subscription(self, data_point: BaseParameterDataPoint) -> None:
1265
+ def remove_event_subscription(self, *, data_point: BaseParameterDataPoint) -> None:
1260
1266
  """Remove event subscription from central collections."""
1261
1267
  if isinstance(data_point, GenericDataPoint | GenericEvent) and data_point.supports_events:
1262
1268
  if data_point.dpk in self._data_point_key_event_subscriptions:
@@ -1264,39 +1270,40 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1264
1270
  if data_point.state_path in self._data_point_path_event_subscriptions:
1265
1271
  del self._data_point_path_event_subscriptions[data_point.state_path]
1266
1272
 
1267
- def get_last_event_seen_for_interface(self, interface_id: str) -> datetime | None:
1273
+ def get_last_event_seen_for_interface(self, *, interface_id: str) -> datetime | None:
1268
1274
  """Return the last event seen for an interface."""
1269
1275
  return self._last_event_seen_for_interface.get(interface_id)
1270
1276
 
1271
- def set_last_event_seen_for_interface(self, interface_id: str) -> None:
1277
+ def set_last_event_seen_for_interface(self, *, interface_id: str) -> None:
1272
1278
  """Set the last event seen for an interface."""
1273
1279
  self._last_event_seen_for_interface[interface_id] = datetime.now()
1274
1280
 
1275
- async def execute_program(self, pid: str) -> bool:
1281
+ async def execute_program(self, *, pid: str) -> bool:
1276
1282
  """Execute a program on the backend."""
1277
1283
  if client := self.primary_client:
1278
1284
  return await client.execute_program(pid=pid)
1279
1285
  return False
1280
1286
 
1281
- async def set_program_state(self, pid: str, state: bool) -> bool:
1287
+ async def set_program_state(self, *, pid: str, state: bool) -> bool:
1282
1288
  """Execute a program on the backend."""
1283
1289
  if client := self.primary_client:
1284
1290
  return await client.set_program_state(pid=pid, state=state)
1285
1291
  return False
1286
1292
 
1287
1293
  @inspector(re_raise=False)
1288
- async def fetch_sysvar_data(self, scheduled: bool) -> None:
1294
+ async def fetch_sysvar_data(self, *, scheduled: bool) -> None:
1289
1295
  """Fetch sysvar data for the hub."""
1290
1296
  await self._hub.fetch_sysvar_data(scheduled=scheduled)
1291
1297
 
1292
1298
  @inspector(re_raise=False)
1293
- async def fetch_program_data(self, scheduled: bool) -> None:
1299
+ async def fetch_program_data(self, *, scheduled: bool) -> None:
1294
1300
  """Fetch program data for the hub."""
1295
1301
  await self._hub.fetch_program_data(scheduled=scheduled)
1296
1302
 
1297
1303
  @inspector(measure_performance=True)
1298
1304
  async def load_and_refresh_data_point_data(
1299
1305
  self,
1306
+ *,
1300
1307
  interface: Interface,
1301
1308
  paramset_key: ParamsetKey | None = None,
1302
1309
  direct_call: bool = False,
@@ -1308,13 +1315,13 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1308
1315
  paramset_key=paramset_key, interface=interface, direct_call=direct_call
1309
1316
  )
1310
1317
 
1311
- async def get_system_variable(self, legacy_name: str) -> Any | None:
1318
+ async def get_system_variable(self, *, legacy_name: str) -> Any | None:
1312
1319
  """Get system variable from the backend."""
1313
1320
  if client := self.primary_client:
1314
- return await client.get_system_variable(legacy_name)
1321
+ return await client.get_system_variable(name=legacy_name)
1315
1322
  return None
1316
1323
 
1317
- async def set_system_variable(self, legacy_name: str, value: Any) -> None:
1324
+ async def set_system_variable(self, *, legacy_name: str, value: Any) -> None:
1318
1325
  """Set variable value on the backend."""
1319
1326
  if dp := self.get_sysvar_data_point(legacy_name=legacy_name):
1320
1327
  await dp.send_variable(value=value)
@@ -1323,6 +1330,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1323
1330
 
1324
1331
  def get_parameters(
1325
1332
  self,
1333
+ *,
1326
1334
  paramset_key: ParamsetKey,
1327
1335
  operations: tuple[Operations, ...],
1328
1336
  full_format: bool = False,
@@ -1400,7 +1408,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1400
1408
 
1401
1409
  return tuple(parameters)
1402
1410
 
1403
- def _get_virtual_remote(self, device_address: str) -> Device | None:
1411
+ def _get_virtual_remote(self, *, device_address: str) -> Device | None:
1404
1412
  """Get the virtual remote for the Client."""
1405
1413
  for client in self._clients.values():
1406
1414
  virtual_remote = client.get_virtual_remote()
@@ -1409,7 +1417,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1409
1417
  return None
1410
1418
 
1411
1419
  def get_generic_data_point(
1412
- self, channel_address: str, parameter: str, paramset_key: ParamsetKey | None = None
1420
+ self, *, channel_address: str, parameter: str, paramset_key: ParamsetKey | None = None
1413
1421
  ) -> GenericDataPoint | None:
1414
1422
  """Get data_point by channel_address and parameter."""
1415
1423
  if device := self.get_device(address=channel_address):
@@ -1418,20 +1426,20 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1418
1426
  )
1419
1427
  return None
1420
1428
 
1421
- def get_event(self, channel_address: str, parameter: str) -> GenericEvent | None:
1429
+ def get_event(self, *, channel_address: str, parameter: str) -> GenericEvent | None:
1422
1430
  """Return the hm event."""
1423
1431
  if device := self.get_device(address=channel_address):
1424
1432
  return device.get_generic_event(channel_address=channel_address, parameter=parameter)
1425
1433
  return None
1426
1434
 
1427
- def get_custom_data_point(self, address: str, channel_no: int) -> CustomDataPoint | None:
1435
+ def get_custom_data_point(self, *, address: str, channel_no: int) -> CustomDataPoint | None:
1428
1436
  """Return the hm custom_data_point."""
1429
1437
  if device := self.get_device(address=address):
1430
1438
  return device.get_custom_data_point(channel_no=channel_no)
1431
1439
  return None
1432
1440
 
1433
1441
  def get_sysvar_data_point(
1434
- self, vid: str | None = None, legacy_name: str | None = None
1442
+ self, *, vid: str | None = None, legacy_name: str | None = None
1435
1443
  ) -> GenericSysvarDataPoint | None:
1436
1444
  """Return the sysvar data_point."""
1437
1445
  if vid and (sysvar := self._sysvar_data_points.get(vid)):
@@ -1442,7 +1450,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1442
1450
  return sysvar
1443
1451
  return None
1444
1452
 
1445
- def get_program_data_point(self, pid: str | None = None, legacy_name: str | None = None) -> ProgramDpType | None:
1453
+ def get_program_data_point(self, *, pid: str | None = None, legacy_name: str | None = None) -> ProgramDpType | None:
1446
1454
  """Return the program data points."""
1447
1455
  if pid and (program := self._program_data_points.get(pid)):
1448
1456
  return program
@@ -1460,7 +1468,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1460
1468
  """Return the registered sysvar state path."""
1461
1469
  return tuple(self._sysvar_data_point_event_subscriptions)
1462
1470
 
1463
- def get_un_ignore_candidates(self, include_master: bool = False) -> list[str]:
1471
+ def get_un_ignore_candidates(self, *, include_master: bool = False) -> list[str]:
1464
1472
  """Return the candidates for un_ignore."""
1465
1473
  candidates = sorted(
1466
1474
  # 1. request simple parameter list for values parameters
@@ -1504,20 +1512,20 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1504
1512
  self._device_details.clear()
1505
1513
  self._data_cache.clear()
1506
1514
 
1507
- def register_homematic_callback(self, cb: Callable) -> CALLBACK_TYPE:
1515
+ def register_homematic_callback(self, *, cb: Callable) -> CALLBACK_TYPE:
1508
1516
  """Register ha_event callback in central."""
1509
1517
  if callable(cb) and cb not in self._homematic_callbacks:
1510
1518
  self._homematic_callbacks.add(cb)
1511
1519
  return partial(self._unregister_homematic_callback, cb=cb)
1512
1520
  return None
1513
1521
 
1514
- def _unregister_homematic_callback(self, cb: Callable) -> None:
1522
+ def _unregister_homematic_callback(self, *, cb: Callable) -> None:
1515
1523
  """RUn register ha_event callback in central."""
1516
1524
  if cb in self._homematic_callbacks:
1517
1525
  self._homematic_callbacks.remove(cb)
1518
1526
 
1519
1527
  @loop_check
1520
- def fire_homematic_callback(self, event_type: EventType, event_data: dict[EventKey, str]) -> None:
1528
+ def fire_homematic_callback(self, *, event_type: EventType, event_data: dict[EventKey, str]) -> None:
1521
1529
  """
1522
1530
  Fire homematic_callback in central.
1523
1531
 
@@ -1525,28 +1533,28 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1525
1533
  """
1526
1534
  for callback_handler in self._homematic_callbacks:
1527
1535
  try:
1528
- callback_handler(event_type, event_data)
1536
+ callback_handler(event_type=event_type, event_data=event_data)
1529
1537
  except Exception as exc:
1530
1538
  _LOGGER.error(
1531
1539
  "FIRE_HOMEMATIC_CALLBACK: Unable to call handler: %s",
1532
1540
  extract_exc_args(exc=exc),
1533
1541
  )
1534
1542
 
1535
- def register_backend_parameter_callback(self, cb: Callable) -> CALLBACK_TYPE:
1543
+ def register_backend_parameter_callback(self, *, cb: Callable) -> CALLBACK_TYPE:
1536
1544
  """Register backend_parameter callback in central."""
1537
1545
  if callable(cb) and cb not in self._backend_parameter_callbacks:
1538
1546
  self._backend_parameter_callbacks.add(cb)
1539
1547
  return partial(self._unregister_backend_parameter_callback, cb=cb)
1540
1548
  return None
1541
1549
 
1542
- def _unregister_backend_parameter_callback(self, cb: Callable) -> None:
1550
+ def _unregister_backend_parameter_callback(self, *, cb: Callable) -> None:
1543
1551
  """Un register backend_parameter callback in central."""
1544
1552
  if cb in self._backend_parameter_callbacks:
1545
1553
  self._backend_parameter_callbacks.remove(cb)
1546
1554
 
1547
1555
  @loop_check
1548
1556
  def fire_backend_parameter_callback(
1549
- self, interface_id: str, channel_address: str, parameter: str, value: Any
1557
+ self, *, interface_id: str, channel_address: str, parameter: str, value: Any
1550
1558
  ) -> None:
1551
1559
  """
1552
1560
  Fire backend_parameter callback in central.
@@ -1555,27 +1563,29 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1555
1563
  """
1556
1564
  for callback_handler in self._backend_parameter_callbacks:
1557
1565
  try:
1558
- callback_handler(interface_id, channel_address, parameter, value)
1566
+ callback_handler(
1567
+ interface_id=interface_id, channel_address=channel_address, parameter=parameter, value=value
1568
+ )
1559
1569
  except Exception as exc:
1560
1570
  _LOGGER.error(
1561
1571
  "FIRE_BACKEND_PARAMETER_CALLBACK: Unable to call handler: %s",
1562
1572
  extract_exc_args(exc=exc),
1563
1573
  )
1564
1574
 
1565
- def register_backend_system_callback(self, cb: Callable) -> CALLBACK_TYPE:
1575
+ def register_backend_system_callback(self, *, cb: Callable) -> CALLBACK_TYPE:
1566
1576
  """Register system_event callback in central."""
1567
1577
  if callable(cb) and cb not in self._backend_parameter_callbacks:
1568
1578
  self._backend_system_callbacks.add(cb)
1569
1579
  return partial(self._unregister_backend_system_callback, cb=cb)
1570
1580
  return None
1571
1581
 
1572
- def _unregister_backend_system_callback(self, cb: Callable) -> None:
1582
+ def _unregister_backend_system_callback(self, *, cb: Callable) -> None:
1573
1583
  """Un register system_event callback in central."""
1574
1584
  if cb in self._backend_system_callbacks:
1575
1585
  self._backend_system_callbacks.remove(cb)
1576
1586
 
1577
1587
  @loop_check
1578
- def fire_backend_system_callback(self, system_event: BackendSystemEvent, **kwargs: Any) -> None:
1588
+ def fire_backend_system_callback(self, *, system_event: BackendSystemEvent, **kwargs: Any) -> None:
1579
1589
  """
1580
1590
  Fire system_event callback in central.
1581
1591
 
@@ -1583,7 +1593,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1583
1593
  """
1584
1594
  for callback_handler in self._backend_system_callbacks:
1585
1595
  try:
1586
- callback_handler(system_event, **kwargs)
1596
+ callback_handler(system_event=system_event, **kwargs)
1587
1597
  except Exception as exc:
1588
1598
  _LOGGER.error(
1589
1599
  "FIRE_BACKEND_SYSTEM_CALLBACK: Unable to call handler: %s",
@@ -1598,7 +1608,7 @@ class CentralUnit(LogContextMixin, PayloadMixin):
1598
1608
  class _Scheduler(threading.Thread):
1599
1609
  """Periodically check connection to the backend, and load data when required."""
1600
1610
 
1601
- def __init__(self, central: CentralUnit) -> None:
1611
+ def __init__(self, *, central: CentralUnit) -> None:
1602
1612
  """Init the connection checker."""
1603
1613
  threading.Thread.__init__(self, name=f"ConnectionChecker for {central.name}")
1604
1614
  self._central: Final = central
@@ -1643,7 +1653,7 @@ class _Scheduler(threading.Thread):
1643
1653
  )
1644
1654
 
1645
1655
  self._central.looper.create_task(
1646
- self._run_scheduler_tasks(),
1656
+ target=self._run_scheduler_tasks(),
1647
1657
  name="run_scheduler_tasks",
1648
1658
  )
1649
1659
 
@@ -1798,6 +1808,7 @@ class _SchedulerJob:
1798
1808
 
1799
1809
  def __init__(
1800
1810
  self,
1811
+ *,
1801
1812
  task: Callable,
1802
1813
  run_interval: int,
1803
1814
  next_run: datetime | None = None,
@@ -1826,6 +1837,7 @@ class CentralConfig:
1826
1837
 
1827
1838
  def __init__(
1828
1839
  self,
1840
+ *,
1829
1841
  central_id: str,
1830
1842
  host: str,
1831
1843
  interface_configs: AbstractSet[hmcl.InterfaceConfig],
@@ -1854,6 +1866,7 @@ class CentralConfig:
1854
1866
  sysvar_markers: tuple[DescriptionMarker | str, ...] = DEFAULT_SYSVAR_MARKERS,
1855
1867
  tls: bool = DEFAULT_TLS,
1856
1868
  un_ignore_list: frozenset[str] = DEFAULT_UN_IGNORES,
1869
+ use_group_channel_for_cover_state: bool = DEFAULT_USE_GROUP_CHANNEL_FOR_COVER_STATE,
1857
1870
  verify_tls: bool = DEFAULT_VERIFY_TLS,
1858
1871
  ) -> None:
1859
1872
  """Init the client config."""
@@ -1884,6 +1897,7 @@ class CentralConfig:
1884
1897
  self.sysvar_markers: Final = sysvar_markers
1885
1898
  self.tls: Final = tls
1886
1899
  self.un_ignore_list: Final = un_ignore_list
1900
+ self.use_group_channel_for_cover_state: Final = use_group_channel_for_cover_state
1887
1901
  self.username: Final = username
1888
1902
  self.verify_tls: Final = verify_tls
1889
1903
 
@@ -1936,7 +1950,7 @@ class CentralConfig:
1936
1950
  """Create the central. Throws BaseHomematicException on validation failure."""
1937
1951
  try:
1938
1952
  self.check_config()
1939
- return CentralUnit(self)
1953
+ return CentralUnit(central_config=self)
1940
1954
  except BaseHomematicException as bhexc:
1941
1955
  raise AioHomematicException(
1942
1956
  f"CREATE_CENTRAL: Not able to create a central: : {extract_exc_args(exc=bhexc)}"
@@ -1950,7 +1964,7 @@ class CentralConfig:
1950
1964
  url = f"{url}:{self.json_port}"
1951
1965
  return f"{url}"
1952
1966
 
1953
- def create_json_rpc_client(self, central: CentralUnit) -> JsonRpcAioHttpClient:
1967
+ def create_json_rpc_client(self, *, central: CentralUnit) -> JsonRpcAioHttpClient:
1954
1968
  """Create a json rpc client."""
1955
1969
  return JsonRpcAioHttpClient(
1956
1970
  username=self.username,
@@ -1971,7 +1985,7 @@ class CentralConnectionState:
1971
1985
  self._json_issues: Final[list[str]] = []
1972
1986
  self._xml_proxy_issues: Final[list[str]] = []
1973
1987
 
1974
- def add_issue(self, issuer: ConnectionProblemIssuer, iid: str) -> bool:
1988
+ def add_issue(self, *, issuer: ConnectionProblemIssuer, iid: str) -> bool:
1975
1989
  """Add issue to collection."""
1976
1990
  if isinstance(issuer, JsonRpcAioHttpClient) and iid not in self._json_issues:
1977
1991
  self._json_issues.append(iid)
@@ -1983,7 +1997,7 @@ class CentralConnectionState:
1983
1997
  return True
1984
1998
  return False
1985
1999
 
1986
- def remove_issue(self, issuer: ConnectionProblemIssuer, iid: str) -> bool:
2000
+ def remove_issue(self, *, issuer: ConnectionProblemIssuer, iid: str) -> bool:
1987
2001
  """Add issue to collection."""
1988
2002
  if isinstance(issuer, JsonRpcAioHttpClient) and iid in self._json_issues:
1989
2003
  self._json_issues.remove(iid)
@@ -1995,7 +2009,7 @@ class CentralConnectionState:
1995
2009
  return True
1996
2010
  return False
1997
2011
 
1998
- def has_issue(self, issuer: ConnectionProblemIssuer, iid: str) -> bool:
2012
+ def has_issue(self, *, issuer: ConnectionProblemIssuer, iid: str) -> bool:
1999
2013
  """Add issue to collection."""
2000
2014
  if isinstance(issuer, JsonRpcAioHttpClient):
2001
2015
  return iid in self._json_issues
@@ -2004,6 +2018,7 @@ class CentralConnectionState:
2004
2018
 
2005
2019
  def handle_exception_log(
2006
2020
  self,
2021
+ *,
2007
2022
  issuer: ConnectionProblemIssuer,
2008
2023
  iid: str,
2009
2024
  exception: Exception,
@@ -2035,6 +2050,7 @@ class CentralConnectionState:
2035
2050
 
2036
2051
 
2037
2052
  def _get_new_data_points(
2053
+ *,
2038
2054
  new_devices: set[Device],
2039
2055
  ) -> Mapping[DataPointCategory, AbstractSet[CallbackDataPoint]]:
2040
2056
  """Return new data points by category."""
@@ -2050,7 +2066,7 @@ def _get_new_data_points(
2050
2066
  return data_points_by_category
2051
2067
 
2052
2068
 
2053
- def _get_new_channel_events(new_devices: set[Device]) -> tuple[tuple[GenericEvent, ...], ...]:
2069
+ def _get_new_channel_events(*, new_devices: set[Device]) -> tuple[tuple[GenericEvent, ...], ...]:
2054
2070
  """Return new channel events by category."""
2055
2071
  channel_events: list[tuple[GenericEvent, ...]] = []
2056
2072