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
@@ -142,7 +142,7 @@ class Hub:
142
142
  "_config",
143
143
  )
144
144
 
145
- def __init__(self, central: hmcu.CentralUnit) -> None:
145
+ def __init__(self, *, central: hmcu.CentralUnit) -> None:
146
146
  """Initialize Homematic hub."""
147
147
  self._sema_fetch_sysvars: Final = asyncio.Semaphore()
148
148
  self._sema_fetch_programs: Final = asyncio.Semaphore()
@@ -150,7 +150,7 @@ class Hub:
150
150
  self._config: Final = central.config
151
151
 
152
152
  @inspector(re_raise=False)
153
- async def fetch_sysvar_data(self, scheduled: bool) -> None:
153
+ async def fetch_sysvar_data(self, *, scheduled: bool) -> None:
154
154
  """Fetch sysvar data for the hub."""
155
155
  if self._config.enable_sysvar_scan:
156
156
  _LOGGER.debug(
@@ -163,7 +163,7 @@ class Hub:
163
163
  await self._update_sysvar_data_points()
164
164
 
165
165
  @inspector(re_raise=False)
166
- async def fetch_program_data(self, scheduled: bool) -> None:
166
+ async def fetch_program_data(self, *, scheduled: bool) -> None:
167
167
  """Fetch program data for the hub."""
168
168
  if self._config.enable_program_scan:
169
169
  _LOGGER.debug(
@@ -206,7 +206,7 @@ class Hub:
206
206
  if new_programs:
207
207
  self._central.fire_backend_system_callback(
208
208
  system_event=BackendSystemEvent.HUB_REFRESHED,
209
- new_hub_data_points=_get_new_hub_data_points(data_points=new_programs),
209
+ new_data_points=_get_new_hub_data_points(data_points=new_programs),
210
210
  )
211
211
 
212
212
  async def _update_sysvar_data_points(self) -> None:
@@ -226,7 +226,7 @@ class Hub:
226
226
  # remove some variables in case of CCU backend
227
227
  # - OldValue(s) are for internal calculations
228
228
  if self._central.model is Backend.CCU:
229
- variables = _clean_variables(variables)
229
+ variables = _clean_variables(variables=variables)
230
230
 
231
231
  if missing_variable_ids := self._identify_missing_variable_ids(variables=variables):
232
232
  self._remove_sysvar_data_point(del_data_point_ids=missing_variable_ids)
@@ -242,10 +242,10 @@ class Hub:
242
242
  if new_sysvars:
243
243
  self._central.fire_backend_system_callback(
244
244
  system_event=BackendSystemEvent.HUB_REFRESHED,
245
- new_hub_data_points=_get_new_hub_data_points(data_points=new_sysvars),
245
+ new_data_points=_get_new_hub_data_points(data_points=new_sysvars),
246
246
  )
247
247
 
248
- def _create_program_dp(self, data: ProgramData) -> ProgramDpType:
248
+ def _create_program_dp(self, *, data: ProgramData) -> ProgramDpType:
249
249
  """Create program as data_point."""
250
250
  program_dp = ProgramDpType(
251
251
  pid=data.pid,
@@ -255,13 +255,13 @@ class Hub:
255
255
  self._central.add_program_data_point(program_dp=program_dp)
256
256
  return program_dp
257
257
 
258
- def _create_system_variable(self, data: SystemVariableData) -> GenericSysvarDataPoint:
258
+ def _create_system_variable(self, *, data: SystemVariableData) -> GenericSysvarDataPoint:
259
259
  """Create system variable as data_point."""
260
260
  sysvar_dp = self._create_sysvar_data_point(data=data)
261
261
  self._central.add_sysvar_data_point(sysvar_data_point=sysvar_dp)
262
262
  return sysvar_dp
263
263
 
264
- def _create_sysvar_data_point(self, data: SystemVariableData) -> GenericSysvarDataPoint:
264
+ def _create_sysvar_data_point(self, *, data: SystemVariableData) -> GenericSysvarDataPoint:
265
265
  """Create sysvar data_point."""
266
266
  data_type = data.data_type
267
267
  extended_sysvar = data.extended_sysvar
@@ -279,21 +279,21 @@ class Hub:
279
279
 
280
280
  return SysvarDpSensor(central=self._central, data=data)
281
281
 
282
- def _remove_program_data_point(self, ids: set[str]) -> None:
282
+ def _remove_program_data_point(self, *, ids: set[str]) -> None:
283
283
  """Remove sysvar data_point from hub."""
284
284
  for pid in ids:
285
285
  self._central.remove_program_button(pid=pid)
286
286
 
287
- def _remove_sysvar_data_point(self, del_data_point_ids: set[str]) -> None:
287
+ def _remove_sysvar_data_point(self, *, del_data_point_ids: set[str]) -> None:
288
288
  """Remove sysvar data_point from hub."""
289
289
  for vid in del_data_point_ids:
290
290
  self._central.remove_sysvar_data_point(vid=vid)
291
291
 
292
- def _identify_missing_program_ids(self, programs: tuple[ProgramData, ...]) -> set[str]:
292
+ def _identify_missing_program_ids(self, *, programs: tuple[ProgramData, ...]) -> set[str]:
293
293
  """Identify missing programs."""
294
294
  return {dp.pid for dp in self._central.program_data_points if dp.pid not in [x.pid for x in programs]}
295
295
 
296
- def _identify_missing_variable_ids(self, variables: tuple[SystemVariableData, ...]) -> set[str]:
296
+ def _identify_missing_variable_ids(self, *, variables: tuple[SystemVariableData, ...]) -> set[str]:
297
297
  """Identify missing variables."""
298
298
  variable_ids: dict[str, bool] = {x.vid: x.extended_sysvar for x in variables}
299
299
  missing_variable_ids: list[str] = []
@@ -307,17 +307,18 @@ class Hub:
307
307
  return set(missing_variable_ids)
308
308
 
309
309
 
310
- def _is_excluded(variable: str, excludes: list[str]) -> bool:
310
+ def _is_excluded(*, variable: str, excludes: list[str]) -> bool:
311
311
  """Check if variable is excluded by exclude_list."""
312
312
  return any(marker in variable for marker in excludes)
313
313
 
314
314
 
315
- def _clean_variables(variables: tuple[SystemVariableData, ...]) -> tuple[SystemVariableData, ...]:
315
+ def _clean_variables(*, variables: tuple[SystemVariableData, ...]) -> tuple[SystemVariableData, ...]:
316
316
  """Clean variables by removing excluded."""
317
- return tuple(sv for sv in variables if not _is_excluded(sv.legacy_name, _EXCLUDED))
317
+ return tuple(sv for sv in variables if not _is_excluded(variable=sv.legacy_name, excludes=_EXCLUDED))
318
318
 
319
319
 
320
320
  def _get_new_hub_data_points(
321
+ *,
321
322
  data_points: Collection[GenericHubDataPoint],
322
323
  ) -> Mapping[DataPointCategory, AbstractSet[GenericHubDataPoint]]:
323
324
  """Return data points as category dict."""
@@ -47,6 +47,7 @@ class GenericHubDataPoint(CallbackDataPoint, PayloadMixin):
47
47
 
48
48
  def __init__(
49
49
  self,
50
+ *,
50
51
  central: hmcu.CentralUnit,
51
52
  address: str,
52
53
  data: HubData,
@@ -132,6 +133,7 @@ class GenericSysvarDataPoint(GenericHubDataPoint):
132
133
 
133
134
  def __init__(
134
135
  self,
136
+ *,
135
137
  central: hmcu.CentralUnit,
136
138
  data: SystemVariableData,
137
139
  ) -> None:
@@ -206,7 +208,7 @@ class GenericSysvarDataPoint(GenericHubDataPoint):
206
208
  """Return the path data of the data_point."""
207
209
  return SysvarPathData(vid=self._vid)
208
210
 
209
- async def event(self, value: Any, received_at: datetime) -> None:
211
+ async def event(self, *, value: Any, received_at: datetime) -> None:
210
212
  """Handle event for which this data_point has subscribed."""
211
213
  self.write_value(value=value, write_at=received_at)
212
214
 
@@ -215,7 +217,7 @@ class GenericSysvarDataPoint(GenericHubDataPoint):
215
217
  self._temporary_value = None
216
218
  self._reset_temporary_timestamps()
217
219
 
218
- def write_value(self, value: Any, write_at: datetime) -> None:
220
+ def write_value(self, *, value: Any, write_at: datetime) -> None:
219
221
  """Set variable value on the backend."""
220
222
  self._reset_temporary_value()
221
223
 
@@ -230,7 +232,7 @@ class GenericSysvarDataPoint(GenericHubDataPoint):
230
232
  self._state_uncertain = False
231
233
  self.fire_data_point_updated_callback()
232
234
 
233
- def _write_temporary_value(self, value: Any, write_at: datetime) -> None:
235
+ def _write_temporary_value(self, *, value: Any, write_at: datetime) -> None:
234
236
  """Update the temporary value of the data_point."""
235
237
  self._reset_temporary_value()
236
238
 
@@ -243,7 +245,7 @@ class GenericSysvarDataPoint(GenericHubDataPoint):
243
245
  self._state_uncertain = True
244
246
  self.fire_data_point_updated_callback()
245
247
 
246
- def _convert_value(self, old_value: Any, new_value: Any) -> Any:
248
+ def _convert_value(self, *, old_value: Any, new_value: Any) -> Any:
247
249
  """Convert to value to SYSVAR_TYPE."""
248
250
  if new_value is None:
249
251
  return None
@@ -261,10 +263,12 @@ class GenericSysvarDataPoint(GenericHubDataPoint):
261
263
  return value
262
264
 
263
265
  @inspector
264
- async def send_variable(self, value: Any) -> None:
266
+ async def send_variable(self, *, value: Any) -> None:
265
267
  """Set variable value on the backend."""
266
268
  if client := self.central.primary_client:
267
- await client.set_system_variable(legacy_name=self._legacy_name, value=parse_sys_var(self._data_type, value))
269
+ await client.set_system_variable(
270
+ legacy_name=self._legacy_name, value=parse_sys_var(data_type=self._data_type, raw_value=value)
271
+ )
268
272
  self._write_temporary_value(value=value, write_at=datetime.now())
269
273
 
270
274
 
@@ -280,6 +284,7 @@ class GenericProgramDataPoint(GenericHubDataPoint):
280
284
 
281
285
  def __init__(
282
286
  self,
287
+ *,
283
288
  central: hmcu.CentralUnit,
284
289
  data: ProgramData,
285
290
  ) -> None:
@@ -315,7 +320,7 @@ class GenericProgramDataPoint(GenericHubDataPoint):
315
320
  """Return the program id."""
316
321
  return self._pid
317
322
 
318
- def update_data(self, data: ProgramData) -> None:
323
+ def update_data(self, *, data: ProgramData) -> None:
319
324
  """Set variable value on the backend."""
320
325
  do_update: bool = False
321
326
  if self._is_active != data.is_active:
@@ -23,11 +23,11 @@ class SysvarDpNumber(GenericSysvarDataPoint):
23
23
  _is_extended = True
24
24
 
25
25
  @inspector
26
- async def send_variable(self, value: float) -> None:
26
+ async def send_variable(self, *, value: float) -> None:
27
27
  """Set the value of the data_point."""
28
28
  if value is not None and self.max is not None and self.min is not None:
29
29
  if self.min <= float(value) <= self.max:
30
- await super().send_variable(value)
30
+ await super().send_variable(value=value)
31
31
  else:
32
32
  _LOGGER.warning(
33
33
  "SYSVAR.NUMBER failed: Invalid value: %s (min: %s, max: %s)",
@@ -36,4 +36,4 @@ class SysvarDpNumber(GenericSysvarDataPoint):
36
36
  self.max,
37
37
  )
38
38
  return
39
- await super().send_variable(value)
39
+ await super().send_variable(value=value)
@@ -32,15 +32,15 @@ class SysvarDpSelect(GenericSysvarDataPoint):
32
32
  return None
33
33
 
34
34
  @inspector
35
- async def send_variable(self, value: int | str) -> None:
35
+ async def send_variable(self, *, value: int | str) -> None:
36
36
  """Set the value of the data_point."""
37
37
  # We allow setting the value via index as well, just in case.
38
38
  if isinstance(value, int) and self._values:
39
39
  if 0 <= value < len(self._values):
40
- await super().send_variable(value)
40
+ await super().send_variable(value=value)
41
41
  elif self._values:
42
42
  if value in self._values:
43
- await super().send_variable(self._values.index(value))
43
+ await super().send_variable(value=self._values.index(value))
44
44
  else:
45
45
  _LOGGER.warning(
46
46
  "Value not in value_list for %s/%s",
@@ -25,6 +25,6 @@ class SysvarDpText(GenericSysvarDataPoint):
25
25
  """Get the value of the data_point."""
26
26
  return cast(str | None, check_length_and_log(name=self._legacy_name, value=self._value))
27
27
 
28
- async def send_variable(self, value: str | None) -> None:
28
+ async def send_variable(self, *, value: str | None) -> None:
29
29
  """Set the value of the data_point."""
30
- await super().send_variable(value)
30
+ await super().send_variable(value=value)
@@ -74,7 +74,7 @@ class ChannelNameData:
74
74
  "sub_device_name",
75
75
  )
76
76
 
77
- def __init__(self, device_name: str, channel_name: str) -> None:
77
+ def __init__(self, *, device_name: str, channel_name: str) -> None:
78
78
  """Init the DataPointNameData class."""
79
79
  self.device_name: Final = device_name
80
80
  self.channel_name: Final = self._get_channel_name(device_name=device_name, channel_name=channel_name)
@@ -105,7 +105,7 @@ class DataPointNameData(ChannelNameData):
105
105
  "parameter_name",
106
106
  )
107
107
 
108
- def __init__(self, device_name: str, channel_name: str, parameter_name: str | None = None) -> None:
108
+ def __init__(self, *, device_name: str, channel_name: str, parameter_name: str | None = None) -> None:
109
109
  """Init the DataPointNameData class."""
110
110
  super().__init__(device_name=device_name, channel_name=channel_name)
111
111
 
@@ -127,7 +127,7 @@ class DataPointNameData(ChannelNameData):
127
127
  return f"{channel_name} {parameter_name}".strip()
128
128
  return channel_name.strip()
129
129
 
130
- def _get_data_point_name(self, device_name: str, channel_name: str, parameter_name: str | None) -> str:
130
+ def _get_data_point_name(self, *, device_name: str, channel_name: str, parameter_name: str | None) -> str:
131
131
  """Return the name of the data_point only name."""
132
132
  channel_parameter_name = self._get_channel_parameter_name(
133
133
  channel_name=channel_name, parameter_name=parameter_name
@@ -145,7 +145,7 @@ class HubNameData:
145
145
  "name",
146
146
  )
147
147
 
148
- def __init__(self, name: str, central_name: str | None = None, channel_name: str | None = None) -> None:
148
+ def __init__(self, *, name: str, central_name: str | None = None, channel_name: str | None = None) -> None:
149
149
  """Init the DataPointNameData class."""
150
150
  self.name: Final = name
151
151
  self.full_name = (
@@ -227,6 +227,7 @@ class DataPointPathData(PathData):
227
227
 
228
228
  def __init__(
229
229
  self,
230
+ *,
230
231
  interface: Interface | None,
231
232
  address: str,
232
233
  channel_no: int | None,
@@ -261,7 +262,7 @@ class ProgramPathData(PathData):
261
262
  "_state_path",
262
263
  )
263
264
 
264
- def __init__(self, pid: str):
265
+ def __init__(self, *, pid: str):
265
266
  """Init the path data."""
266
267
  self._set_path: Final = f"{PROGRAM_SET_PATH_ROOT}/{pid}"
267
268
  self._state_path: Final = f"{PROGRAM_STATE_PATH_ROOT}/{pid}"
@@ -285,7 +286,7 @@ class SysvarPathData(PathData):
285
286
  "_state_path",
286
287
  )
287
288
 
288
- def __init__(self, vid: str):
289
+ def __init__(self, *, vid: str):
289
290
  """Init the path data."""
290
291
  self._set_path: Final = f"{SYSVAR_SET_PATH_ROOT}/{vid}"
291
292
  self._state_path: Final = f"{SYSVAR_STATE_PATH_ROOT}/{vid}"
@@ -527,7 +528,7 @@ def _convert_value_noncached(value: Any, target_type: ParameterType, value_list:
527
528
  # relevant for ENUMs retyped to a BOOL
528
529
  return _get_binary_sensor_value(value=value, value_list=value_list)
529
530
  if isinstance(value, str):
530
- return to_bool(value)
531
+ return to_bool(value=value)
531
532
  return bool(value)
532
533
  if target_type == ParameterType.FLOAT:
533
534
  return float(value)
@@ -39,7 +39,7 @@ class DpUpdate(CallbackDataPoint, PayloadMixin):
39
39
 
40
40
  _category = DataPointCategory.UPDATE
41
41
 
42
- def __init__(self, device: hmd.Device) -> None:
42
+ def __init__(self, *, device: hmd.Device) -> None:
43
43
  """Init the callback data_point."""
44
44
  PayloadMixin.__init__(self)
45
45
  self._device: Final = device
@@ -112,7 +112,7 @@ class DpUpdate(CallbackDataPoint, PayloadMixin):
112
112
  kind=DataPointCategory.UPDATE,
113
113
  )
114
114
 
115
- def register_data_point_updated_callback(self, cb: Callable, custom_id: str) -> CALLBACK_TYPE:
115
+ def register_data_point_updated_callback(self, *, cb: Callable, custom_id: str) -> CALLBACK_TYPE:
116
116
  """Register update callback."""
117
117
  if custom_id != DEFAULT_CUSTOM_ID:
118
118
  if self._custom_id is not None:
@@ -121,18 +121,18 @@ class DpUpdate(CallbackDataPoint, PayloadMixin):
121
121
  )
122
122
  self._custom_id = custom_id
123
123
 
124
- if self._device.register_firmware_update_callback(cb) is not None:
124
+ if self._device.register_firmware_update_callback(cb=cb) is not None:
125
125
  return partial(self._unregister_data_point_updated_callback, cb=cb, custom_id=custom_id)
126
126
  return None
127
127
 
128
- def _unregister_data_point_updated_callback(self, cb: Callable, custom_id: str) -> None:
128
+ def _unregister_data_point_updated_callback(self, *, cb: Callable, custom_id: str) -> None:
129
129
  """Unregister update callback."""
130
130
  if custom_id is not None:
131
131
  self._custom_id = None
132
- self._device.unregister_firmware_update_callback(cb)
132
+ self._device.unregister_firmware_update_callback(cb=cb)
133
133
 
134
134
  @inspector
135
- async def update_firmware(self, refresh_after_update_intervals: tuple[int, ...]) -> bool:
135
+ async def update_firmware(self, *, refresh_after_update_intervals: tuple[int, ...]) -> bool:
136
136
  """Turn the update on."""
137
137
  return await self._device.update_firmware(refresh_after_update_intervals=refresh_after_update_intervals)
138
138
 
aiohomematic/support.py CHANGED
@@ -54,7 +54,7 @@ from aiohomematic.property_decorators import Kind, get_hm_property_by_kind, get_
54
54
  _LOGGER: Final = logging.getLogger(__name__)
55
55
 
56
56
 
57
- def extract_exc_args(exc: Exception) -> tuple[Any, ...] | Any:
57
+ def extract_exc_args(*, exc: Exception) -> tuple[Any, ...] | Any:
58
58
  """Return the first arg, if there is only one arg."""
59
59
  if exc.args:
60
60
  return exc.args[0] if len(exc.args) == 1 else exc.args
@@ -62,6 +62,7 @@ def extract_exc_args(exc: Exception) -> tuple[Any, ...] | Any:
62
62
 
63
63
 
64
64
  def build_xml_rpc_uri(
65
+ *,
65
66
  host: str,
66
67
  port: int | None,
67
68
  path: str | None,
@@ -80,6 +81,7 @@ def build_xml_rpc_uri(
80
81
 
81
82
 
82
83
  def build_xml_rpc_headers(
84
+ *,
83
85
  username: str,
84
86
  password: str,
85
87
  ) -> list[tuple[str, str]]:
@@ -90,6 +92,7 @@ def build_xml_rpc_headers(
90
92
 
91
93
 
92
94
  def check_config(
95
+ *,
93
96
  central_name: str,
94
97
  host: str,
95
98
  username: str,
@@ -111,10 +114,10 @@ def check_config(
111
114
  config_failures.append("Username must not be empty")
112
115
  if not password:
113
116
  config_failures.append("Password is required")
114
- if not check_password(password):
117
+ if not check_password(password=password):
115
118
  config_failures.append("Password is not valid")
116
119
  try:
117
- check_or_create_directory(storage_folder)
120
+ check_or_create_directory(directory=storage_folder)
118
121
  except BaseHomematicException as bhexc:
119
122
  config_failures.append(extract_exc_args(exc=bhexc)[0])
120
123
  if callback_host and not (is_hostname(hostname=callback_host) or is_ipv4_address(address=callback_host)):
@@ -129,7 +132,7 @@ def check_config(
129
132
  return config_failures
130
133
 
131
134
 
132
- def has_primary_client(interface_configs: AbstractSet[hmcl.InterfaceConfig]) -> bool:
135
+ def has_primary_client(*, interface_configs: AbstractSet[hmcl.InterfaceConfig]) -> bool:
133
136
  """Check if all configured clients exists in central."""
134
137
  for interface_config in interface_configs:
135
138
  if interface_config.interface in PRIMARY_CLIENT_CANDIDATE_INTERFACES:
@@ -137,7 +140,7 @@ def has_primary_client(interface_configs: AbstractSet[hmcl.InterfaceConfig]) ->
137
140
  return False
138
141
 
139
142
 
140
- def delete_file(folder: str, file_name: str) -> None:
143
+ def delete_file(*, folder: str, file_name: str) -> None:
141
144
  """Delete the file."""
142
145
  file_path = os.path.join(folder, file_name)
143
146
  if (
@@ -148,7 +151,7 @@ def delete_file(folder: str, file_name: str) -> None:
148
151
  os.unlink(file_path)
149
152
 
150
153
 
151
- def check_or_create_directory(directory: str) -> bool:
154
+ def check_or_create_directory(*, directory: str) -> bool:
152
155
  """Check / create directory."""
153
156
  if not directory:
154
157
  return False
@@ -162,12 +165,12 @@ def check_or_create_directory(directory: str) -> bool:
162
165
  return True
163
166
 
164
167
 
165
- def parse_sys_var(data_type: SysvarType | None, raw_value: Any) -> Any:
168
+ def parse_sys_var(*, data_type: SysvarType | None, raw_value: Any) -> Any:
166
169
  """Parse system variables to fix type."""
167
170
  if not data_type:
168
171
  return raw_value
169
172
  if data_type in (SysvarType.ALARM, SysvarType.LOGIC):
170
- return to_bool(raw_value)
173
+ return to_bool(value=raw_value)
171
174
  if data_type == SysvarType.FLOAT:
172
175
  return float(raw_value)
173
176
  if data_type in (SysvarType.INTEGER, SysvarType.LIST):
@@ -175,7 +178,7 @@ def parse_sys_var(data_type: SysvarType | None, raw_value: Any) -> Any:
175
178
  return raw_value
176
179
 
177
180
 
178
- def to_bool(value: Any) -> bool:
181
+ def to_bool(*, value: Any) -> bool:
179
182
  """Convert defined string values to bool."""
180
183
  if isinstance(value, bool):
181
184
  return value
@@ -186,7 +189,7 @@ def to_bool(value: Any) -> bool:
186
189
  return value.lower() in ["y", "yes", "t", "true", "on", "1"]
187
190
 
188
191
 
189
- def check_password(password: str | None) -> bool:
192
+ def check_password(*, password: str | None) -> bool:
190
193
  """Check password."""
191
194
  if password is None:
192
195
  return False
@@ -200,7 +203,7 @@ def check_password(password: str | None) -> bool:
200
203
  return True
201
204
 
202
205
 
203
- def regular_to_default_dict_hook(origin: dict) -> defaultdict[Any, Any]:
206
+ def regular_to_default_dict_hook(origin: dict, /) -> defaultdict[Any, Any]:
204
207
  """Use defaultdict in json.loads object_hook."""
205
208
  new_dict: Callable = lambda: defaultdict(new_dict)
206
209
  new_instance = new_dict()
@@ -208,7 +211,7 @@ def regular_to_default_dict_hook(origin: dict) -> defaultdict[Any, Any]:
208
211
  return cast(defaultdict[Any, Any], new_instance)
209
212
 
210
213
 
211
- def _create_tls_context(verify_tls: bool) -> ssl.SSLContext:
214
+ def _create_tls_context(*, verify_tls: bool) -> ssl.SSLContext:
212
215
  """Create tls verified/unverified context."""
213
216
  sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
214
217
  if not verify_tls:
@@ -225,48 +228,48 @@ _DEFAULT_NO_VERIFY_SSL_CONTEXT = _create_tls_context(verify_tls=False)
225
228
  _DEFAULT_SSL_CONTEXT = _create_tls_context(verify_tls=True)
226
229
 
227
230
 
228
- def get_tls_context(verify_tls: bool) -> ssl.SSLContext:
231
+ def get_tls_context(*, verify_tls: bool) -> ssl.SSLContext:
229
232
  """Return tls verified/unverified context."""
230
233
  return _DEFAULT_SSL_CONTEXT if verify_tls else _DEFAULT_NO_VERIFY_SSL_CONTEXT
231
234
 
232
235
 
233
- def get_channel_address(device_address: str, channel_no: int | None) -> str:
236
+ def get_channel_address(*, device_address: str, channel_no: int | None) -> str:
234
237
  """Return the channel address."""
235
238
  return device_address if channel_no is None else f"{device_address}:{channel_no}"
236
239
 
237
240
 
238
- def get_device_address(address: str) -> str:
241
+ def get_device_address(*, address: str) -> str:
239
242
  """Return the device part of an address."""
240
243
  return get_split_channel_address(channel_address=address)[0]
241
244
 
242
245
 
243
- def get_channel_no(address: str) -> int | None:
246
+ def get_channel_no(*, address: str) -> int | None:
244
247
  """Return the channel part of an address."""
245
248
  return get_split_channel_address(channel_address=address)[1]
246
249
 
247
250
 
248
- def is_address(address: str) -> bool:
251
+ def is_address(*, address: str) -> bool:
249
252
  """Check if it is a address."""
250
253
  return is_device_address(address=address) or is_channel_address(address=address)
251
254
 
252
255
 
253
- def is_channel_address(address: str) -> bool:
256
+ def is_channel_address(*, address: str) -> bool:
254
257
  """Check if it is a channel address."""
255
258
  return CHANNEL_ADDRESS_PATTERN.match(address) is not None
256
259
 
257
260
 
258
- def is_device_address(address: str) -> bool:
261
+ def is_device_address(*, address: str) -> bool:
259
262
  """Check if it is a device address."""
260
263
  return DEVICE_ADDRESS_PATTERN.match(address) is not None
261
264
 
262
265
 
263
- def is_paramset_key(paramset_key: ParamsetKey | str) -> bool:
266
+ def is_paramset_key(*, paramset_key: ParamsetKey | str) -> bool:
264
267
  """Check if it is a paramset key."""
265
268
  return isinstance(paramset_key, ParamsetKey) or (isinstance(paramset_key, str) and paramset_key in ParamsetKey)
266
269
 
267
270
 
268
271
  @lru_cache(maxsize=4096)
269
- def get_split_channel_address(channel_address: str) -> tuple[str, int | None]:
272
+ def get_split_channel_address(*, channel_address: str) -> tuple[str, int | None]:
270
273
  """
271
274
  Return the device part of an address.
272
275
 
@@ -281,7 +284,7 @@ def get_split_channel_address(channel_address: str) -> tuple[str, int | None]:
281
284
  return channel_address, None
282
285
 
283
286
 
284
- def changed_within_seconds(last_change: datetime, max_age: int = MAX_CACHE_AGE) -> bool:
287
+ def changed_within_seconds(*, last_change: datetime, max_age: int = MAX_CACHE_AGE) -> bool:
285
288
  """DataPoint has been modified within X minutes."""
286
289
  if last_change == INIT_DATETIME:
287
290
  return False
@@ -297,7 +300,7 @@ def find_free_port() -> int:
297
300
  return int(sock.getsockname()[1])
298
301
 
299
302
 
300
- def get_ip_addr(host: str, port: int) -> str | None:
303
+ def get_ip_addr(host: str, port: int, /) -> str | None:
301
304
  """Get local_ip from socket."""
302
305
  try:
303
306
  socket.gethostbyname(host)
@@ -314,7 +317,7 @@ def get_ip_addr(host: str, port: int) -> str | None:
314
317
  return local_ip
315
318
 
316
319
 
317
- def is_hostname(hostname: str | None) -> bool:
320
+ def is_hostname(*, hostname: str | None) -> bool:
318
321
  """Return True if hostname is valid."""
319
322
  if not hostname:
320
323
  return False
@@ -333,7 +336,7 @@ def is_hostname(hostname: str | None) -> bool:
333
336
  return all(ALLOWED_HOSTNAME_PATTERN.match(label) for label in labels)
334
337
 
335
338
 
336
- def is_ipv4_address(address: str | None) -> bool:
339
+ def is_ipv4_address(*, address: str | None) -> bool:
337
340
  """Return True if ipv4_address is valid."""
338
341
  if not address:
339
342
  return False
@@ -344,12 +347,13 @@ def is_ipv4_address(address: str | None) -> bool:
344
347
  return True
345
348
 
346
349
 
347
- def is_port(port: int) -> bool:
350
+ def is_port(*, port: int) -> bool:
348
351
  """Return True if port is valid."""
349
352
  return 0 <= port <= 65535
350
353
 
351
354
 
352
355
  def element_matches_key(
356
+ *,
353
357
  search_elements: str | Collection[str],
354
358
  compare_with: str | None,
355
359
  search_key: str | None = None,
@@ -401,7 +405,7 @@ def element_matches_key(
401
405
  return False
402
406
 
403
407
 
404
- def _get_search_key(search_elements: Collection[str], search_key: str) -> str | None:
408
+ def _get_search_key(*, search_elements: Collection[str], search_key: str) -> str | None:
405
409
  """Search for a matching key in a collection."""
406
410
  for element in search_elements:
407
411
  if search_key.startswith(element):
@@ -446,7 +450,7 @@ def debug_enabled() -> bool:
446
450
  return False
447
451
 
448
452
 
449
- def hash_sha256(value: Any) -> str:
453
+ def hash_sha256(*, value: Any) -> str:
450
454
  """
451
455
  Hash a value with sha256.
452
456
 
@@ -459,26 +463,26 @@ def hash_sha256(value: Any) -> str:
459
463
  data = orjson.dumps(value, option=orjson.OPT_SORT_KEYS | orjson.OPT_NON_STR_KEYS)
460
464
  except Exception:
461
465
  # Fallback: convert to a hashable representation and use repr()
462
- data = repr(_make_value_hashable(value)).encode()
466
+ data = repr(_make_value_hashable(value=value)).encode()
463
467
  hasher.update(data)
464
468
  return base64.b64encode(hasher.digest()).decode()
465
469
 
466
470
 
467
- def _make_value_hashable(value: Any) -> Any:
471
+ def _make_value_hashable(*, value: Any) -> Any:
468
472
  """Make a hashable object."""
469
473
  if isinstance(value, tuple | list):
470
- return tuple(_make_value_hashable(e) for e in value)
474
+ return tuple(_make_value_hashable(value=e) for e in value)
471
475
 
472
476
  if isinstance(value, dict):
473
- return tuple(sorted((k, _make_value_hashable(v)) for k, v in value.items()))
477
+ return tuple(sorted((k, _make_value_hashable(value=v)) for k, v in value.items()))
474
478
 
475
479
  if isinstance(value, set | frozenset):
476
- return tuple(sorted(_make_value_hashable(e) for e in value))
480
+ return tuple(sorted(_make_value_hashable(value=e) for e in value))
477
481
 
478
482
  return value
479
483
 
480
484
 
481
- def get_rx_modes(mode: int) -> tuple[RxMode, ...]:
485
+ def get_rx_modes(*, mode: int) -> tuple[RxMode, ...]:
482
486
  """Convert int to rx modes."""
483
487
  rx_modes: set[RxMode] = set()
484
488
  if mode & RxMode.LAZY_CONFIG:
@@ -498,14 +502,14 @@ def get_rx_modes(mode: int) -> tuple[RxMode, ...]:
498
502
  return tuple(rx_modes)
499
503
 
500
504
 
501
- def supports_rx_mode(command_rx_mode: CommandRxMode, rx_modes: tuple[RxMode, ...]) -> bool:
505
+ def supports_rx_mode(*, command_rx_mode: CommandRxMode, rx_modes: tuple[RxMode, ...]) -> bool:
502
506
  """Check if rx mode is supported."""
503
507
  return (command_rx_mode == CommandRxMode.BURST and RxMode.BURST in rx_modes) or (
504
508
  command_rx_mode == CommandRxMode.WAKEUP and RxMode.WAKEUP in rx_modes
505
509
  )
506
510
 
507
511
 
508
- def cleanup_text_from_html_tags(text: str) -> str:
512
+ def cleanup_text_from_html_tags(*, text: str) -> str:
509
513
  """Cleanup text from html tags."""
510
514
  return re.sub(HTMLTAG_PATTERN, "", text)
511
515
 
@@ -515,7 +519,7 @@ def cleanup_text_from_html_tags(text: str) -> str:
515
519
  _BOUNDARY_MSG = "error_boundary"
516
520
 
517
521
 
518
- def _safe_log_context(context: Mapping[str, Any] | None) -> dict[str, Any]:
522
+ def _safe_log_context(*, context: Mapping[str, Any] | None) -> dict[str, Any]:
519
523
  """Extract safe context from a mapping."""
520
524
  ctx: dict[str, Any] = {}
521
525
  if not context:
@@ -565,7 +569,9 @@ def log_boundary_error(
565
569
  log_message += f" {message}"
566
570
 
567
571
  if log_context:
568
- log_message += f" ctx={orjson.dumps(_safe_log_context(log_context), option=orjson.OPT_SORT_KEYS).decode()}"
572
+ log_message += (
573
+ f" ctx={orjson.dumps(_safe_log_context(context=log_context), option=orjson.OPT_SORT_KEYS).decode()}"
574
+ )
569
575
 
570
576
  # Choose level if not provided:
571
577
  if (chosen_level := level) is None: