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.
- 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 +87 -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 +2 -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 +32 -18
- 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.1.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.1.dist-info/RECORD +0 -78
- {aiohomematic-2025.10.1.dist-info → aiohomematic-2025.10.2.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.10.1.dist-info → aiohomematic-2025.10.2.dist-info}/licenses/LICENSE +0 -0
- {aiohomematic-2025.10.1.dist-info → aiohomematic-2025.10.2.dist-info}/top_level.txt +0 -0
aiohomematic/client/json_rpc.py
CHANGED
|
@@ -184,6 +184,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
184
184
|
|
|
185
185
|
def __init__(
|
|
186
186
|
self,
|
|
187
|
+
*,
|
|
187
188
|
username: str,
|
|
188
189
|
password: str,
|
|
189
190
|
device_url: str,
|
|
@@ -204,7 +205,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
204
205
|
self._password: Final = password
|
|
205
206
|
self._looper = Looper()
|
|
206
207
|
self._tls: Final = tls
|
|
207
|
-
self._tls_context: Final[SSLContext | bool] = get_tls_context(verify_tls) if tls else False
|
|
208
|
+
self._tls_context: Final[SSLContext | bool] = get_tls_context(verify_tls=verify_tls) if tls else False
|
|
208
209
|
self._url: Final = f"{device_url}{PATH_JSON_RPC}"
|
|
209
210
|
self._script_cache: Final[dict[str, str]] = {}
|
|
210
211
|
self._last_session_id_refresh: datetime | None = None
|
|
@@ -234,10 +235,10 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
234
235
|
self._last_session_id_refresh = datetime.now()
|
|
235
236
|
return self._session_id is not None
|
|
236
237
|
if self._session_id:
|
|
237
|
-
self._session_id = await self._do_renew_login(self._session_id)
|
|
238
|
+
self._session_id = await self._do_renew_login(session_id=self._session_id)
|
|
238
239
|
return self._session_id is not None
|
|
239
240
|
|
|
240
|
-
async def _do_renew_login(self, session_id: str) -> str | None:
|
|
241
|
+
async def _do_renew_login(self, *, session_id: str) -> str | None:
|
|
241
242
|
"""Renew JSON-RPC session or perform login."""
|
|
242
243
|
if self._has_session_recently_refreshed:
|
|
243
244
|
return session_id
|
|
@@ -291,6 +292,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
291
292
|
|
|
292
293
|
async def _post(
|
|
293
294
|
self,
|
|
295
|
+
*,
|
|
294
296
|
method: _JsonRpcMethod,
|
|
295
297
|
extra_params: dict[_JsonKey, Any] | None = None,
|
|
296
298
|
use_default_params: bool = True,
|
|
@@ -328,6 +330,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
328
330
|
|
|
329
331
|
async def _post_script(
|
|
330
332
|
self,
|
|
333
|
+
*,
|
|
331
334
|
script_name: str,
|
|
332
335
|
extra_params: dict[_JsonKey, Any] | None = None,
|
|
333
336
|
keep_session: bool = True,
|
|
@@ -370,7 +373,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
370
373
|
|
|
371
374
|
return response
|
|
372
375
|
|
|
373
|
-
async def _get_script(self, script_name: str) -> str | None:
|
|
376
|
+
async def _get_script(self, *, script_name: str) -> str | None:
|
|
374
377
|
"""Return a script from the script cache. Load if required."""
|
|
375
378
|
if script_name in self._script_cache:
|
|
376
379
|
return self._script_cache[script_name]
|
|
@@ -387,6 +390,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
387
390
|
|
|
388
391
|
async def _do_post(
|
|
389
392
|
self,
|
|
393
|
+
*,
|
|
390
394
|
session_id: bool | str,
|
|
391
395
|
method: _JsonRpcMethod,
|
|
392
396
|
extra_params: dict[_JsonKey, Any] | None = None,
|
|
@@ -527,7 +531,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
527
531
|
)
|
|
528
532
|
raise ClientException(exc) from exc
|
|
529
533
|
|
|
530
|
-
async def _get_json_reponse(self, response: ClientResponse) -> dict[str, Any] | Any:
|
|
534
|
+
async def _get_json_reponse(self, *, response: ClientResponse) -> dict[str, Any] | Any:
|
|
531
535
|
"""Return the json object from response."""
|
|
532
536
|
try:
|
|
533
537
|
return await response.json(encoding=UTF_8)
|
|
@@ -543,7 +547,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
543
547
|
"""Logout of the backend."""
|
|
544
548
|
try:
|
|
545
549
|
await self._looper.block_till_done()
|
|
546
|
-
await self._do_logout(self._session_id)
|
|
550
|
+
await self._do_logout(session_id=self._session_id)
|
|
547
551
|
except BaseHomematicException:
|
|
548
552
|
_LOGGER.debug("LOGOUT: logout failed")
|
|
549
553
|
|
|
@@ -552,7 +556,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
552
556
|
if self._is_internal_session:
|
|
553
557
|
await self._client_session.close()
|
|
554
558
|
|
|
555
|
-
async def _do_logout(self, session_id: str | None) -> None:
|
|
559
|
+
async def _do_logout(self, *, session_id: str | None) -> None:
|
|
556
560
|
"""Logout of the backend."""
|
|
557
561
|
if not session_id:
|
|
558
562
|
_LOGGER.debug("DO_LOGOUT: Not logged in. Not logging out.")
|
|
@@ -579,7 +583,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
579
583
|
"""Clear the current session."""
|
|
580
584
|
self._session_id = None
|
|
581
585
|
|
|
582
|
-
async def execute_program(self, pid: str) -> bool:
|
|
586
|
+
async def execute_program(self, *, pid: str) -> bool:
|
|
583
587
|
"""Execute a program on the backend."""
|
|
584
588
|
params = {
|
|
585
589
|
_JsonKey.ID: pid,
|
|
@@ -596,7 +600,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
596
600
|
|
|
597
601
|
return True
|
|
598
602
|
|
|
599
|
-
async def set_program_state(self, pid: str, state: bool) -> bool:
|
|
603
|
+
async def set_program_state(self, *, pid: str, state: bool) -> bool:
|
|
600
604
|
"""Set the program state on the backend."""
|
|
601
605
|
params = {
|
|
602
606
|
_JsonKey.ID: pid,
|
|
@@ -613,7 +617,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
613
617
|
|
|
614
618
|
return True
|
|
615
619
|
|
|
616
|
-
async def set_system_variable(self, legacy_name: str, value: Any) -> bool:
|
|
620
|
+
async def set_system_variable(self, *, legacy_name: str, value: Any) -> bool:
|
|
617
621
|
"""Set a system variable on the backend."""
|
|
618
622
|
params = {_JsonKey.NAME: legacy_name, _JsonKey.VALUE: value}
|
|
619
623
|
if isinstance(value, bool):
|
|
@@ -639,7 +643,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
639
643
|
|
|
640
644
|
return True
|
|
641
645
|
|
|
642
|
-
async def delete_system_variable(self, name: str) -> bool:
|
|
646
|
+
async def delete_system_variable(self, *, name: str) -> bool:
|
|
643
647
|
"""Delete a system variable from the backend."""
|
|
644
648
|
params = {_JsonKey.NAME: name}
|
|
645
649
|
response = await self._post(
|
|
@@ -654,7 +658,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
654
658
|
|
|
655
659
|
return True
|
|
656
660
|
|
|
657
|
-
async def get_system_variable(self, name: str) -> Any:
|
|
661
|
+
async def get_system_variable(self, *, name: str) -> Any:
|
|
658
662
|
"""Get single system variable from the backend."""
|
|
659
663
|
params = {_JsonKey.NAME: name}
|
|
660
664
|
response = await self._post(
|
|
@@ -666,7 +670,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
666
670
|
return response[_JsonKey.RESULT]
|
|
667
671
|
|
|
668
672
|
async def get_all_system_variables(
|
|
669
|
-
self, markers: tuple[DescriptionMarker | str, ...]
|
|
673
|
+
self, *, markers: tuple[DescriptionMarker | str, ...]
|
|
670
674
|
) -> tuple[SystemVariableData, ...]:
|
|
671
675
|
"""Get all system variables from the backend."""
|
|
672
676
|
variables: list[SystemVariableData] = []
|
|
@@ -843,7 +847,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
843
847
|
|
|
844
848
|
return channel_ids_function
|
|
845
849
|
|
|
846
|
-
async def get_device_description(self, interface: Interface, address: str) -> DeviceDescription | None:
|
|
850
|
+
async def get_device_description(self, *, interface: Interface, address: str) -> DeviceDescription | None:
|
|
847
851
|
"""Get device descriptions from the backend."""
|
|
848
852
|
device_description: DeviceDescription | None = None
|
|
849
853
|
params = {
|
|
@@ -860,7 +864,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
860
864
|
return device_description
|
|
861
865
|
|
|
862
866
|
@staticmethod
|
|
863
|
-
def _convert_device_description(json_data: dict[str, Any]) -> DeviceDescription:
|
|
867
|
+
def _convert_device_description(*, json_data: dict[str, Any]) -> DeviceDescription:
|
|
864
868
|
"""Convert json data dor device description."""
|
|
865
869
|
device_description = DeviceDescription(
|
|
866
870
|
TYPE=json_data["type"],
|
|
@@ -904,7 +908,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
904
908
|
return device_details
|
|
905
909
|
|
|
906
910
|
async def get_paramset(
|
|
907
|
-
self, interface: Interface, address: str, paramset_key: ParamsetKey | str
|
|
911
|
+
self, *, interface: Interface, address: str, paramset_key: ParamsetKey | str
|
|
908
912
|
) -> dict[str, Any] | None:
|
|
909
913
|
"""Get paramset from the backend."""
|
|
910
914
|
paramset: dict[str, Any] = {}
|
|
@@ -927,6 +931,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
927
931
|
|
|
928
932
|
async def put_paramset(
|
|
929
933
|
self,
|
|
934
|
+
*,
|
|
930
935
|
interface: Interface,
|
|
931
936
|
address: str,
|
|
932
937
|
paramset_key: ParamsetKey | str,
|
|
@@ -952,7 +957,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
952
957
|
str(json_result),
|
|
953
958
|
)
|
|
954
959
|
|
|
955
|
-
async def get_value(self, interface: Interface, address: str, paramset_key: ParamsetKey, parameter: str) -> Any:
|
|
960
|
+
async def get_value(self, *, interface: Interface, address: str, paramset_key: ParamsetKey, parameter: str) -> Any:
|
|
956
961
|
"""Get value from the backend."""
|
|
957
962
|
value: Any = None
|
|
958
963
|
params = {
|
|
@@ -973,7 +978,9 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
973
978
|
|
|
974
979
|
return value
|
|
975
980
|
|
|
976
|
-
async def set_value(
|
|
981
|
+
async def set_value(
|
|
982
|
+
self, *, interface: Interface, address: str, parameter: str, value_type: str, value: Any
|
|
983
|
+
) -> None:
|
|
977
984
|
"""Set value to the backend."""
|
|
978
985
|
params = {
|
|
979
986
|
_JsonKey.INTERFACE: interface,
|
|
@@ -996,7 +1003,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
996
1003
|
)
|
|
997
1004
|
|
|
998
1005
|
async def get_paramset_description(
|
|
999
|
-
self, interface: Interface, address: str, paramset_key: ParamsetKey
|
|
1006
|
+
self, *, interface: Interface, address: str, paramset_key: ParamsetKey
|
|
1000
1007
|
) -> Mapping[str, ParameterData] | None:
|
|
1001
1008
|
"""Get paramset description from the backend."""
|
|
1002
1009
|
paramset_description: dict[str, ParameterData] = {}
|
|
@@ -1015,7 +1022,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
1015
1022
|
return paramset_description
|
|
1016
1023
|
|
|
1017
1024
|
@staticmethod
|
|
1018
|
-
def _convert_parameter_data(json_data: dict[str, Any]) -> ParameterData:
|
|
1025
|
+
def _convert_parameter_data(*, json_data: dict[str, Any]) -> ParameterData:
|
|
1019
1026
|
"""Convert json data to parameter data."""
|
|
1020
1027
|
|
|
1021
1028
|
_type = json_data["TYPE"]
|
|
@@ -1039,7 +1046,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
1039
1046
|
|
|
1040
1047
|
return parameter_data
|
|
1041
1048
|
|
|
1042
|
-
async def get_all_device_data(self, interface: Interface) -> Mapping[str, Any]:
|
|
1049
|
+
async def get_all_device_data(self, *, interface: Interface) -> Mapping[str, Any]:
|
|
1043
1050
|
"""Get the all device data of the backend."""
|
|
1044
1051
|
all_device_data: dict[str, Any] = {}
|
|
1045
1052
|
params = {
|
|
@@ -1064,7 +1071,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
1064
1071
|
|
|
1065
1072
|
return all_device_data
|
|
1066
1073
|
|
|
1067
|
-
async def get_all_programs(self, markers: tuple[DescriptionMarker | str, ...]) -> tuple[ProgramData, ...]:
|
|
1074
|
+
async def get_all_programs(self, *, markers: tuple[DescriptionMarker | str, ...]) -> tuple[ProgramData, ...]:
|
|
1068
1075
|
"""Get the all programs of the backend."""
|
|
1069
1076
|
all_programs: list[ProgramData] = []
|
|
1070
1077
|
|
|
@@ -1118,7 +1125,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
1118
1125
|
|
|
1119
1126
|
return tuple(all_programs)
|
|
1120
1127
|
|
|
1121
|
-
async def is_present(self, interface: Interface) -> bool:
|
|
1128
|
+
async def is_present(self, *, interface: Interface) -> bool:
|
|
1122
1129
|
"""Get value from the backend."""
|
|
1123
1130
|
value: bool = False
|
|
1124
1131
|
params = {_JsonKey.INTERFACE: interface}
|
|
@@ -1131,7 +1138,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
1131
1138
|
|
|
1132
1139
|
return value
|
|
1133
1140
|
|
|
1134
|
-
async def has_program_ids(self, channel_hmid: str) -> bool:
|
|
1141
|
+
async def has_program_ids(self, *, channel_hmid: str) -> bool:
|
|
1135
1142
|
"""Return if a channel has program ids."""
|
|
1136
1143
|
params = {_JsonKey.ID: channel_hmid}
|
|
1137
1144
|
response = await self._post(
|
|
@@ -1206,7 +1213,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
1206
1213
|
|
|
1207
1214
|
return True
|
|
1208
1215
|
|
|
1209
|
-
async def list_devices(self, interface: Interface) -> tuple[DeviceDescription, ...]:
|
|
1216
|
+
async def list_devices(self, *, interface: Interface) -> tuple[DeviceDescription, ...]:
|
|
1210
1217
|
"""List devices from the backend."""
|
|
1211
1218
|
devices: tuple[DeviceDescription, ...] = ()
|
|
1212
1219
|
_LOGGER.debug("LIST_DEVICES: Getting all available interfaces")
|
|
@@ -1262,6 +1269,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
1262
1269
|
|
|
1263
1270
|
|
|
1264
1271
|
def _get_params(
|
|
1272
|
+
*,
|
|
1265
1273
|
session_id: bool | str,
|
|
1266
1274
|
extra_params: dict[_JsonKey, Any] | None,
|
|
1267
1275
|
use_default_params: bool,
|
aiohomematic/client/xml_rpc.py
CHANGED
|
@@ -88,11 +88,14 @@ class XmlRpcProxy(xmlrpc.client.ServerProxy):
|
|
|
88
88
|
|
|
89
89
|
def __init__(
|
|
90
90
|
self,
|
|
91
|
+
*,
|
|
91
92
|
max_workers: int,
|
|
92
93
|
interface_id: str,
|
|
93
94
|
connection_state: hmcu.CentralConnectionState,
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
uri: str,
|
|
96
|
+
headers: list[tuple[str, str]],
|
|
97
|
+
tls: bool = False,
|
|
98
|
+
verify_tls: bool = False,
|
|
96
99
|
) -> None:
|
|
97
100
|
"""Initialize new proxy for server and get local ip."""
|
|
98
101
|
self._interface_id: Final = interface_id
|
|
@@ -101,17 +104,19 @@ class XmlRpcProxy(xmlrpc.client.ServerProxy):
|
|
|
101
104
|
self._proxy_executor: Final = (
|
|
102
105
|
ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix=interface_id) if max_workers > 0 else None
|
|
103
106
|
)
|
|
104
|
-
self._tls: Final[bool] =
|
|
105
|
-
self._verify_tls: Final[bool] =
|
|
107
|
+
self._tls: Final[bool] = tls
|
|
108
|
+
self._verify_tls: Final[bool] = verify_tls
|
|
106
109
|
self._supported_methods: tuple[str, ...] = ()
|
|
110
|
+
kwargs: dict[str, Any] = {}
|
|
107
111
|
if self._tls:
|
|
108
|
-
kwargs[_CONTEXT] = get_tls_context(self._verify_tls)
|
|
112
|
+
kwargs[_CONTEXT] = get_tls_context(verify_tls=self._verify_tls)
|
|
109
113
|
# Due to magic method the log_context must be defined manually.
|
|
110
114
|
self.log_context: Final[Mapping[str, Any]] = {"interface_id": self._interface_id, "tls": self._tls}
|
|
111
|
-
xmlrpc.client.ServerProxy.__init__(
|
|
115
|
+
xmlrpc.client.ServerProxy.__init__(
|
|
112
116
|
self,
|
|
117
|
+
uri=uri,
|
|
113
118
|
encoding=ISO_8859_1,
|
|
114
|
-
|
|
119
|
+
headers=headers,
|
|
115
120
|
**kwargs,
|
|
116
121
|
)
|
|
117
122
|
|
|
@@ -234,7 +239,7 @@ def _cleanup_args(*args: Any) -> Any:
|
|
|
234
239
|
return args
|
|
235
240
|
|
|
236
241
|
|
|
237
|
-
def _cleanup_item(item: Any) -> Any:
|
|
242
|
+
def _cleanup_item(*, item: Any) -> Any:
|
|
238
243
|
"""Cleanup a single item."""
|
|
239
244
|
if isinstance(item, StrEnum):
|
|
240
245
|
return str(item)
|
|
@@ -245,7 +250,7 @@ def _cleanup_item(item: Any) -> Any:
|
|
|
245
250
|
return item
|
|
246
251
|
|
|
247
252
|
|
|
248
|
-
def _cleanup_paramset(paramset: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
253
|
+
def _cleanup_paramset(*, paramset: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
249
254
|
"""Cleanup a paramset."""
|
|
250
255
|
new_paramset: dict[str, Any] = {}
|
|
251
256
|
for name, value in paramset.items():
|
aiohomematic/const.py
CHANGED
|
@@ -19,7 +19,7 @@ import sys
|
|
|
19
19
|
from types import MappingProxyType
|
|
20
20
|
from typing import Any, Final, NamedTuple, Required, TypeAlias, TypedDict
|
|
21
21
|
|
|
22
|
-
VERSION: Final = "2025.10.
|
|
22
|
+
VERSION: Final = "2025.10.2"
|
|
23
23
|
|
|
24
24
|
# Detect test speedup mode via environment
|
|
25
25
|
_TEST_SPEEDUP: Final = (
|
|
@@ -100,6 +100,7 @@ IDENTIFIER_SEPARATOR: Final = "@"
|
|
|
100
100
|
INIT_DATETIME: Final = datetime.strptime("01.01.1970 00:00:00", DATETIME_FORMAT)
|
|
101
101
|
IP_ANY_V4: Final = "0.0.0.0"
|
|
102
102
|
JSON_SESSION_AGE: Final = 90
|
|
103
|
+
KWARGS_ARG_CUSTOM_ID: Final = "custom_id"
|
|
103
104
|
KWARGS_ARG_DATA_POINT: Final = "data_point"
|
|
104
105
|
LAST_COMMAND_SEND_STORE_TIMEOUT: Final = 60
|
|
105
106
|
LOCAL_HOST: Final = "127.0.0.1"
|
aiohomematic/converter.py
CHANGED
|
@@ -21,23 +21,23 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@lru_cache(maxsize=1024)
|
|
24
|
-
def _convert_cpv_to_hm_level(
|
|
24
|
+
def _convert_cpv_to_hm_level(*, value: Any) -> Any:
|
|
25
25
|
"""Convert combined parameter value for hm level."""
|
|
26
|
-
if isinstance(
|
|
27
|
-
return ast.literal_eval(
|
|
28
|
-
return
|
|
26
|
+
if isinstance(value, str) and value.startswith("0x"):
|
|
27
|
+
return ast.literal_eval(value) / 100 / 2
|
|
28
|
+
return value
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@lru_cache(maxsize=1024)
|
|
32
|
-
def _convert_cpv_to_hmip_level(
|
|
32
|
+
def _convert_cpv_to_hmip_level(*, value: Any) -> Any:
|
|
33
33
|
"""Convert combined parameter value for hmip level."""
|
|
34
|
-
return int(
|
|
34
|
+
return int(value) / 100
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@lru_cache(maxsize=1024)
|
|
38
|
-
def convert_hm_level_to_cpv(
|
|
38
|
+
def convert_hm_level_to_cpv(*, value: Any) -> Any:
|
|
39
39
|
"""Convert hm level to combined parameter value."""
|
|
40
|
-
return format(int(
|
|
40
|
+
return format(int(value * 100 * 2), "#04x")
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
CONVERTABLE_PARAMETERS: Final = (Parameter.COMBINED_PARAMETER, Parameter.LEVEL_COMBINED)
|
|
@@ -52,28 +52,28 @@ _COMBINED_PARAMETER_NAMES: Final = {"L": Parameter.LEVEL, "L2": Parameter.LEVEL_
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
@lru_cache(maxsize=1024)
|
|
55
|
-
def _convert_combined_parameter_to_paramset(
|
|
55
|
+
def _convert_combined_parameter_to_paramset(*, value: str) -> dict[str, Any]:
|
|
56
56
|
"""Convert combined parameter to paramset."""
|
|
57
57
|
paramset: dict[str, Any] = {}
|
|
58
|
-
for cp_param_value in
|
|
58
|
+
for cp_param_value in value.split(","):
|
|
59
59
|
cp_param, value = cp_param_value.split("=")
|
|
60
60
|
if parameter := _COMBINED_PARAMETER_NAMES.get(cp_param):
|
|
61
61
|
if converter := _COMBINED_PARAMETER_TO_HM_CONVERTER.get(parameter):
|
|
62
|
-
paramset[parameter] = converter(value)
|
|
62
|
+
paramset[parameter] = converter(value=value)
|
|
63
63
|
else:
|
|
64
64
|
paramset[parameter] = value
|
|
65
65
|
return paramset
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
@lru_cache(maxsize=1024)
|
|
69
|
-
def _convert_level_combined_to_paramset(
|
|
69
|
+
def _convert_level_combined_to_paramset(*, value: str) -> dict[str, Any]:
|
|
70
70
|
"""Convert combined parameter to paramset."""
|
|
71
|
-
if "," in
|
|
72
|
-
l1_value, l2_value =
|
|
71
|
+
if "," in value:
|
|
72
|
+
l1_value, l2_value = value.split(",")
|
|
73
73
|
if converter := _COMBINED_PARAMETER_TO_HM_CONVERTER.get(Parameter.LEVEL_COMBINED):
|
|
74
74
|
return {
|
|
75
|
-
Parameter.LEVEL: converter(l1_value),
|
|
76
|
-
Parameter.LEVEL_SLATS: converter(l2_value),
|
|
75
|
+
Parameter.LEVEL: converter(value=l1_value),
|
|
76
|
+
Parameter.LEVEL_SLATS: converter(value=l2_value),
|
|
77
77
|
}
|
|
78
78
|
return {}
|
|
79
79
|
|
|
@@ -85,12 +85,12 @@ _COMBINED_PARAMETER_TO_PARAMSET_CONVERTER: Final = {
|
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
@lru_cache(maxsize=1024)
|
|
88
|
-
def convert_combined_parameter_to_paramset(parameter: str,
|
|
88
|
+
def convert_combined_parameter_to_paramset(*, parameter: str, value: str) -> dict[str, Any]:
|
|
89
89
|
"""Convert combined parameter to paramset."""
|
|
90
90
|
try:
|
|
91
91
|
if converter := _COMBINED_PARAMETER_TO_PARAMSET_CONVERTER.get(parameter): # type: ignore[call-overload]
|
|
92
|
-
return cast(dict[str, Any], converter(
|
|
93
|
-
_LOGGER.debug("CONVERT_COMBINED_PARAMETER_TO_PARAMSET: No converter found for %s: %s", parameter,
|
|
92
|
+
return cast(dict[str, Any], converter(value=value))
|
|
93
|
+
_LOGGER.debug("CONVERT_COMBINED_PARAMETER_TO_PARAMSET: No converter found for %s: %s", parameter, value)
|
|
94
94
|
except Exception as exc:
|
|
95
95
|
_LOGGER.debug("CONVERT_COMBINED_PARAMETER_TO_PARAMSET: Convert failed %s", extract_exc_args(exc=exc))
|
|
96
96
|
return {}
|
aiohomematic/exceptions.py
CHANGED
|
@@ -102,12 +102,13 @@ class InternalBackendException(BaseHomematicException):
|
|
|
102
102
|
super().__init__("InternalBackendException", *args)
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
def _reduce_args(args: tuple[Any, ...]) -> tuple[Any, ...] | Any:
|
|
105
|
+
def _reduce_args(*, args: tuple[Any, ...]) -> tuple[Any, ...] | Any:
|
|
106
106
|
"""Return the first arg, if there is only one arg."""
|
|
107
107
|
return args[0] if len(args) == 1 else args
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
def log_exception[**P, R](
|
|
111
|
+
*,
|
|
111
112
|
exc_type: type[BaseException],
|
|
112
113
|
logger: logging.Logger = _LOGGER,
|
|
113
114
|
level: int = logging.ERROR,
|
aiohomematic/model/__init__.py
CHANGED
|
@@ -46,7 +46,7 @@ _LOGGER: Final = logging.getLogger(__name__)
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
@inspector
|
|
49
|
-
def create_data_points_and_events(device: hmd.Device) -> None:
|
|
49
|
+
def create_data_points_and_events(*, device: hmd.Device) -> None:
|
|
50
50
|
"""Create the data points associated to this device."""
|
|
51
51
|
for channel in device.channels.values():
|
|
52
52
|
for paramset_key, paramsset_key_descriptions in channel.paramset_descriptions.items():
|
|
@@ -83,6 +83,7 @@ def create_data_points_and_events(device: hmd.Device) -> None:
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
def _process_parameter(
|
|
86
|
+
*,
|
|
86
87
|
channel: hmd.Channel,
|
|
87
88
|
paramset_key: ParamsetKey,
|
|
88
89
|
parameter: str,
|
|
@@ -119,7 +120,7 @@ def _process_parameter(
|
|
|
119
120
|
)
|
|
120
121
|
|
|
121
122
|
|
|
122
|
-
def _should_create_event(parameter_data: ParameterData, parameter: str) -> bool:
|
|
123
|
+
def _should_create_event(*, parameter_data: ParameterData, parameter: str) -> bool:
|
|
123
124
|
"""Determine if an event should be created for the parameter."""
|
|
124
125
|
return bool(
|
|
125
126
|
parameter_data["OPERATIONS"] & Operations.EVENT
|
|
@@ -127,7 +128,7 @@ def _should_create_event(parameter_data: ParameterData, parameter: str) -> bool:
|
|
|
127
128
|
)
|
|
128
129
|
|
|
129
130
|
|
|
130
|
-
def _should_skip_data_point(parameter_data: ParameterData, parameter: str, parameter_is_un_ignored: bool) -> bool:
|
|
131
|
+
def _should_skip_data_point(*, parameter_data: ParameterData, parameter: str, parameter_is_un_ignored: bool) -> bool:
|
|
131
132
|
"""Determine if a data point should be skipped."""
|
|
132
133
|
return bool(
|
|
133
134
|
(not parameter_data["OPERATIONS"] & Operations.EVENT and not parameter_data["OPERATIONS"] & Operations.WRITE)
|
|
@@ -60,7 +60,7 @@ _LOGGER: Final = logging.getLogger(__name__)
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
@inspector
|
|
63
|
-
def create_calculated_data_points(channel: hmd.Channel) -> None:
|
|
63
|
+
def create_calculated_data_points(*, channel: hmd.Channel) -> None:
|
|
64
64
|
"""Decides which data point category should be used, and creates the required data points."""
|
|
65
65
|
for dp in _CALCULATED_DATA_POINTS:
|
|
66
66
|
if dp.is_relevant_for_model(channel=channel):
|
|
@@ -34,7 +34,7 @@ class BaseClimateSensor[SensorT: float | None](CalculatedDataPoint[SensorT]):
|
|
|
34
34
|
|
|
35
35
|
_category = DataPointCategory.SENSOR
|
|
36
36
|
|
|
37
|
-
def __init__(self, channel: hmd.Channel) -> None:
|
|
37
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
38
38
|
"""Initialize the data point."""
|
|
39
39
|
|
|
40
40
|
super().__init__(channel=channel)
|
|
@@ -70,7 +70,7 @@ class ApparentTemperature(BaseClimateSensor):
|
|
|
70
70
|
|
|
71
71
|
_calculated_parameter = CalulatedParameter.APPARENT_TEMPERATURE
|
|
72
72
|
|
|
73
|
-
def __init__(self, channel: hmd.Channel) -> None:
|
|
73
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
74
74
|
"""Initialize the data point."""
|
|
75
75
|
super().__init__(channel=channel)
|
|
76
76
|
self._unit = "°C"
|
|
@@ -83,7 +83,7 @@ class ApparentTemperature(BaseClimateSensor):
|
|
|
83
83
|
)
|
|
84
84
|
|
|
85
85
|
@staticmethod
|
|
86
|
-
def is_relevant_for_model(channel: hmd.Channel) -> bool:
|
|
86
|
+
def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
|
|
87
87
|
"""Return if this calculated data point is relevant for the model."""
|
|
88
88
|
return (
|
|
89
89
|
element_matches_key(
|
|
@@ -120,13 +120,13 @@ class DewPoint(BaseClimateSensor):
|
|
|
120
120
|
|
|
121
121
|
_calculated_parameter = CalulatedParameter.DEW_POINT
|
|
122
122
|
|
|
123
|
-
def __init__(self, channel: hmd.Channel) -> None:
|
|
123
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
124
124
|
"""Initialize the data point."""
|
|
125
125
|
super().__init__(channel=channel)
|
|
126
126
|
self._unit = "°C"
|
|
127
127
|
|
|
128
128
|
@staticmethod
|
|
129
|
-
def is_relevant_for_model(channel: hmd.Channel) -> bool:
|
|
129
|
+
def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
|
|
130
130
|
"""Return if this calculated data point is relevant for the model."""
|
|
131
131
|
return _is_relevant_for_model_temperature_and_humidity(channel=channel)
|
|
132
132
|
|
|
@@ -148,13 +148,13 @@ class FrostPoint(BaseClimateSensor):
|
|
|
148
148
|
|
|
149
149
|
_calculated_parameter = CalulatedParameter.FROST_POINT
|
|
150
150
|
|
|
151
|
-
def __init__(self, channel: hmd.Channel) -> None:
|
|
151
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
152
152
|
"""Initialize the data point."""
|
|
153
153
|
super().__init__(channel=channel)
|
|
154
154
|
self._unit = "°C"
|
|
155
155
|
|
|
156
156
|
@staticmethod
|
|
157
|
-
def is_relevant_for_model(channel: hmd.Channel) -> bool:
|
|
157
|
+
def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
|
|
158
158
|
"""Return if this calculated data point is relevant for the model."""
|
|
159
159
|
return _is_relevant_for_model_temperature_and_humidity(
|
|
160
160
|
channel=channel, relevant_models=_RELEVANT_MODELS_FROST_POINT
|
|
@@ -178,13 +178,13 @@ class VaporConcentration(BaseClimateSensor):
|
|
|
178
178
|
|
|
179
179
|
_calculated_parameter = CalulatedParameter.VAPOR_CONCENTRATION
|
|
180
180
|
|
|
181
|
-
def __init__(self, channel: hmd.Channel) -> None:
|
|
181
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
182
182
|
"""Initialize the data point."""
|
|
183
183
|
super().__init__(channel=channel)
|
|
184
184
|
self._unit = "g/m³"
|
|
185
185
|
|
|
186
186
|
@staticmethod
|
|
187
|
-
def is_relevant_for_model(channel: hmd.Channel) -> bool:
|
|
187
|
+
def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
|
|
188
188
|
"""Return if this calculated data point is relevant for the model."""
|
|
189
189
|
return _is_relevant_for_model_temperature_and_humidity(channel=channel)
|
|
190
190
|
|
|
@@ -59,6 +59,7 @@ class CalculatedDataPoint[ParameterT: GenericParameterType](BaseDataPoint):
|
|
|
59
59
|
|
|
60
60
|
def __init__(
|
|
61
61
|
self,
|
|
62
|
+
*,
|
|
62
63
|
channel: hmd.Channel,
|
|
63
64
|
) -> None:
|
|
64
65
|
"""Initialize the data point."""
|
|
@@ -92,7 +93,7 @@ class CalculatedDataPoint[ParameterT: GenericParameterType](BaseDataPoint):
|
|
|
92
93
|
)
|
|
93
94
|
|
|
94
95
|
def _add_data_point[DataPointT: hmge.GenericDataPoint](
|
|
95
|
-
self, parameter: str, paramset_key: ParamsetKey | None, data_point_type: type[DataPointT]
|
|
96
|
+
self, *, parameter: str, paramset_key: ParamsetKey | None, data_point_type: type[DataPointT]
|
|
96
97
|
) -> DataPointT:
|
|
97
98
|
"""Add a new data point."""
|
|
98
99
|
if generic_data_point := self._channel.get_generic_data_point(parameter=parameter, paramset_key=paramset_key):
|
|
@@ -109,7 +110,12 @@ class CalculatedDataPoint[ParameterT: GenericParameterType](BaseDataPoint):
|
|
|
109
110
|
)
|
|
110
111
|
|
|
111
112
|
def _add_device_data_point[DataPointT: hmge.GenericDataPoint](
|
|
112
|
-
self,
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
channel_address: str,
|
|
116
|
+
parameter: str,
|
|
117
|
+
paramset_key: ParamsetKey | None,
|
|
118
|
+
data_point_type: type[DataPointT],
|
|
113
119
|
) -> DataPointT:
|
|
114
120
|
"""Add a new data point."""
|
|
115
121
|
if generic_data_point := self._channel.device.get_generic_data_point(
|
|
@@ -133,7 +139,7 @@ class CalculatedDataPoint[ParameterT: GenericParameterType](BaseDataPoint):
|
|
|
133
139
|
return bool(self._operations & Operations.READ)
|
|
134
140
|
|
|
135
141
|
@staticmethod
|
|
136
|
-
def is_relevant_for_model(channel: hmd.Channel) -> bool:
|
|
142
|
+
def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
|
|
137
143
|
"""Return if this calculated data point is relevant for the channel."""
|
|
138
144
|
return False
|
|
139
145
|
|
|
@@ -286,7 +292,7 @@ class CalculatedDataPoint[ParameterT: GenericParameterType](BaseDataPoint):
|
|
|
286
292
|
"""Return the signature of the data_point."""
|
|
287
293
|
return f"{self._category}/{self._channel.device.model}/{self._calculated_parameter}"
|
|
288
294
|
|
|
289
|
-
async def load_data_point_value(self, call_source: CallSource, direct_call: bool = False) -> None:
|
|
295
|
+
async def load_data_point_value(self, *, call_source: CallSource, direct_call: bool = False) -> None:
|
|
290
296
|
"""Init the data point values."""
|
|
291
297
|
for dp in self._readable_data_points:
|
|
292
298
|
await dp.load_data_point_value(call_source=call_source, direct_call=direct_call)
|
|
@@ -306,7 +312,7 @@ class CalculatedDataPoint[ParameterT: GenericParameterType](BaseDataPoint):
|
|
|
306
312
|
@property
|
|
307
313
|
def _should_fire_data_point_updated_callback(self) -> bool:
|
|
308
314
|
"""Check if a data point has been updated or refreshed."""
|
|
309
|
-
if self.
|
|
315
|
+
if self.fired_event_recently: # pylint: disable=using-constant-test
|
|
310
316
|
return False
|
|
311
317
|
|
|
312
318
|
if (relevant_values_data_point := self._relevant_values_data_points) is not None and len(
|
|
@@ -314,9 +320,9 @@ class CalculatedDataPoint[ParameterT: GenericParameterType](BaseDataPoint):
|
|
|
314
320
|
) <= 1:
|
|
315
321
|
return True
|
|
316
322
|
|
|
317
|
-
return all(dp.
|
|
323
|
+
return all(dp.fired_event_recently for dp in relevant_values_data_point)
|
|
318
324
|
|
|
319
|
-
def _unregister_data_point_updated_callback(self, cb: Callable, custom_id: str) -> None:
|
|
325
|
+
def _unregister_data_point_updated_callback(self, *, cb: Callable, custom_id: str) -> None:
|
|
320
326
|
"""Unregister update callback."""
|
|
321
327
|
for unregister in self._unregister_callbacks:
|
|
322
328
|
if unregister is not None:
|
|
@@ -41,7 +41,7 @@ class OperatingVoltageLevel[SensorT: float | None](CalculatedDataPoint[SensorT])
|
|
|
41
41
|
_calculated_parameter = CalulatedParameter.OPERATING_VOLTAGE_LEVEL
|
|
42
42
|
_category = DataPointCategory.SENSOR
|
|
43
43
|
|
|
44
|
-
def __init__(self, channel: hmd.Channel) -> None:
|
|
44
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
45
45
|
"""Initialize the data point."""
|
|
46
46
|
super().__init__(channel=channel)
|
|
47
47
|
self._type = ParameterType.FLOAT
|
|
@@ -89,7 +89,7 @@ class OperatingVoltageLevel[SensorT: float | None](CalculatedDataPoint[SensorT])
|
|
|
89
89
|
)
|
|
90
90
|
|
|
91
91
|
@staticmethod
|
|
92
|
-
def is_relevant_for_model(channel: hmd.Channel) -> bool:
|
|
92
|
+
def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
|
|
93
93
|
"""Return if this calculated data point is relevant for the model."""
|
|
94
94
|
if element_matches_key(
|
|
95
95
|
search_elements=_IGNORE_OPERATING_VOLTAGE_LEVEL_MODELS, compare_with=channel.device.model
|