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.
- aiohomematic/async_support.py +7 -7
- aiohomematic/caches/dynamic.py +31 -26
- aiohomematic/caches/persistent.py +34 -32
- aiohomematic/caches/visibility.py +19 -7
- aiohomematic/central/__init__.py +90 -74
- aiohomematic/central/decorators.py +2 -2
- aiohomematic/central/xml_rpc_server.py +27 -24
- aiohomematic/client/__init__.py +72 -56
- aiohomematic/client/_rpc_errors.py +3 -3
- aiohomematic/client/json_rpc.py +33 -25
- aiohomematic/client/xml_rpc.py +14 -9
- aiohomematic/const.py +3 -1
- aiohomematic/converter.py +19 -19
- aiohomematic/exceptions.py +2 -1
- aiohomematic/model/__init__.py +4 -3
- aiohomematic/model/calculated/__init__.py +1 -1
- aiohomematic/model/calculated/climate.py +9 -9
- aiohomematic/model/calculated/data_point.py +13 -7
- aiohomematic/model/calculated/operating_voltage_level.py +2 -2
- aiohomematic/model/calculated/support.py +7 -7
- aiohomematic/model/custom/__init__.py +3 -3
- aiohomematic/model/custom/climate.py +57 -34
- aiohomematic/model/custom/cover.py +44 -20
- aiohomematic/model/custom/data_point.py +9 -7
- aiohomematic/model/custom/definition.py +23 -17
- aiohomematic/model/custom/light.py +52 -23
- aiohomematic/model/custom/lock.py +16 -12
- aiohomematic/model/custom/siren.py +6 -3
- aiohomematic/model/custom/switch.py +3 -2
- aiohomematic/model/custom/valve.py +3 -2
- aiohomematic/model/data_point.py +62 -49
- aiohomematic/model/device.py +48 -42
- aiohomematic/model/event.py +6 -5
- aiohomematic/model/generic/__init__.py +6 -4
- aiohomematic/model/generic/action.py +1 -1
- aiohomematic/model/generic/data_point.py +7 -5
- aiohomematic/model/generic/number.py +3 -3
- aiohomematic/model/generic/select.py +1 -1
- aiohomematic/model/generic/sensor.py +2 -2
- aiohomematic/model/generic/switch.py +3 -3
- aiohomematic/model/hub/__init__.py +17 -16
- aiohomematic/model/hub/data_point.py +12 -7
- aiohomematic/model/hub/number.py +3 -3
- aiohomematic/model/hub/select.py +3 -3
- aiohomematic/model/hub/text.py +2 -2
- aiohomematic/model/support.py +8 -7
- aiohomematic/model/update.py +6 -6
- aiohomematic/support.py +44 -38
- aiohomematic/validator.py +6 -6
- {aiohomematic-2025.10.0.dist-info → aiohomematic-2025.10.2.dist-info}/METADATA +1 -1
- aiohomematic-2025.10.2.dist-info/RECORD +78 -0
- aiohomematic_support/client_local.py +19 -12
- aiohomematic-2025.10.0.dist-info/RECORD +0 -78
- {aiohomematic-2025.10.0.dist-info → aiohomematic-2025.10.2.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.10.0.dist-info → aiohomematic-2025.10.2.dist-info}/licenses/LICENSE +0 -0
- {aiohomematic-2025.10.0.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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
aiohomematic/model/hub/number.py
CHANGED
|
@@ -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)
|
aiohomematic/model/hub/select.py
CHANGED
|
@@ -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",
|
aiohomematic/model/hub/text.py
CHANGED
|
@@ -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)
|
aiohomematic/model/support.py
CHANGED
|
@@ -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)
|
aiohomematic/model/update.py
CHANGED
|
@@ -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 +=
|
|
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:
|