aiohomematic 2025.8.8__py3-none-any.whl → 2025.8.10__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/__init__.py +15 -1
- aiohomematic/async_support.py +15 -2
- aiohomematic/caches/__init__.py +2 -0
- aiohomematic/caches/dynamic.py +2 -0
- aiohomematic/caches/persistent.py +29 -22
- aiohomematic/caches/visibility.py +277 -252
- aiohomematic/central/__init__.py +69 -49
- aiohomematic/central/decorators.py +60 -15
- aiohomematic/central/xml_rpc_server.py +15 -1
- aiohomematic/client/__init__.py +2 -0
- aiohomematic/client/_rpc_errors.py +81 -0
- aiohomematic/client/json_rpc.py +68 -19
- aiohomematic/client/xml_rpc.py +15 -8
- aiohomematic/const.py +145 -77
- aiohomematic/context.py +11 -1
- aiohomematic/converter.py +27 -1
- aiohomematic/decorators.py +88 -19
- aiohomematic/exceptions.py +19 -1
- aiohomematic/hmcli.py +13 -1
- aiohomematic/model/__init__.py +2 -0
- aiohomematic/model/calculated/__init__.py +2 -0
- aiohomematic/model/calculated/climate.py +2 -0
- aiohomematic/model/calculated/data_point.py +7 -1
- aiohomematic/model/calculated/operating_voltage_level.py +2 -0
- aiohomematic/model/calculated/support.py +2 -0
- aiohomematic/model/custom/__init__.py +2 -0
- aiohomematic/model/custom/climate.py +3 -1
- aiohomematic/model/custom/const.py +2 -0
- aiohomematic/model/custom/cover.py +30 -2
- aiohomematic/model/custom/data_point.py +6 -0
- aiohomematic/model/custom/definition.py +2 -0
- aiohomematic/model/custom/light.py +18 -10
- aiohomematic/model/custom/lock.py +2 -0
- aiohomematic/model/custom/siren.py +5 -2
- aiohomematic/model/custom/support.py +2 -0
- aiohomematic/model/custom/switch.py +2 -0
- aiohomematic/model/custom/valve.py +2 -0
- aiohomematic/model/data_point.py +30 -3
- aiohomematic/model/decorators.py +29 -8
- aiohomematic/model/device.py +9 -5
- aiohomematic/model/event.py +2 -0
- aiohomematic/model/generic/__init__.py +2 -0
- aiohomematic/model/generic/action.py +2 -0
- aiohomematic/model/generic/binary_sensor.py +2 -0
- aiohomematic/model/generic/button.py +2 -0
- aiohomematic/model/generic/data_point.py +4 -1
- aiohomematic/model/generic/number.py +4 -1
- aiohomematic/model/generic/select.py +4 -1
- aiohomematic/model/generic/sensor.py +2 -0
- aiohomematic/model/generic/switch.py +2 -0
- aiohomematic/model/generic/text.py +2 -0
- aiohomematic/model/hub/__init__.py +2 -0
- aiohomematic/model/hub/binary_sensor.py +2 -0
- aiohomematic/model/hub/button.py +2 -0
- aiohomematic/model/hub/data_point.py +6 -0
- aiohomematic/model/hub/number.py +2 -0
- aiohomematic/model/hub/select.py +2 -0
- aiohomematic/model/hub/sensor.py +2 -0
- aiohomematic/model/hub/switch.py +2 -0
- aiohomematic/model/hub/text.py +2 -0
- aiohomematic/model/support.py +26 -1
- aiohomematic/model/update.py +6 -0
- aiohomematic/support.py +175 -5
- aiohomematic/validator.py +49 -2
- aiohomematic-2025.8.10.dist-info/METADATA +124 -0
- aiohomematic-2025.8.10.dist-info/RECORD +78 -0
- {aiohomematic-2025.8.8.dist-info → aiohomematic-2025.8.10.dist-info}/licenses/LICENSE +1 -1
- aiohomematic-2025.8.8.dist-info/METADATA +0 -69
- aiohomematic-2025.8.8.dist-info/RECORD +0 -77
- {aiohomematic-2025.8.8.dist-info → aiohomematic-2025.8.10.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.8.8.dist-info → aiohomematic-2025.8.10.dist-info}/top_level.txt +0 -0
aiohomematic/client/json_rpc.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""
|
|
2
4
|
Asynchronous JSON-RPC client for HomeMatic CCU-compatible backends.
|
|
3
5
|
|
|
@@ -29,6 +31,7 @@ Notes
|
|
|
29
31
|
|
|
30
32
|
from __future__ import annotations
|
|
31
33
|
|
|
34
|
+
import asyncio
|
|
32
35
|
from asyncio import Semaphore
|
|
33
36
|
from collections.abc import Mapping
|
|
34
37
|
from datetime import datetime
|
|
@@ -54,6 +57,7 @@ import orjson
|
|
|
54
57
|
|
|
55
58
|
from aiohomematic import central as hmcu
|
|
56
59
|
from aiohomematic.async_support import Looper
|
|
60
|
+
from aiohomematic.client._rpc_errors import RpcContext, map_jsonrpc_error
|
|
57
61
|
from aiohomematic.const import (
|
|
58
62
|
ALWAYS_ENABLE_SYSVARS_BY_ID,
|
|
59
63
|
DEFAULT_INCLUDE_INTERNAL_PROGRAMS,
|
|
@@ -78,7 +82,6 @@ from aiohomematic.const import (
|
|
|
78
82
|
SysvarType,
|
|
79
83
|
)
|
|
80
84
|
from aiohomematic.exceptions import (
|
|
81
|
-
AuthFailure,
|
|
82
85
|
BaseHomematicException,
|
|
83
86
|
ClientException,
|
|
84
87
|
InternalBackendException,
|
|
@@ -91,6 +94,7 @@ from aiohomematic.support import (
|
|
|
91
94
|
element_matches_key,
|
|
92
95
|
extract_exc_args,
|
|
93
96
|
get_tls_context,
|
|
97
|
+
log_boundary_error,
|
|
94
98
|
parse_sys_var,
|
|
95
99
|
)
|
|
96
100
|
|
|
@@ -402,38 +406,59 @@ class JsonRpcAioHttpClient:
|
|
|
402
406
|
)
|
|
403
407
|
if method in _PARALLEL_EXECUTION_LIMITED_JSONRPC_METHODS:
|
|
404
408
|
async with self._sema:
|
|
405
|
-
if (response := await post_call()) is None:
|
|
409
|
+
if (response := await asyncio.shield(post_call())) is None:
|
|
406
410
|
raise ClientException("POST method failed with no response")
|
|
407
|
-
elif (response := await post_call()) is None:
|
|
411
|
+
elif (response := await asyncio.shield(post_call())) is None:
|
|
408
412
|
raise ClientException("POST method failed with no response")
|
|
409
413
|
|
|
410
414
|
if response.status == 200:
|
|
411
|
-
json_response = await self._get_json_reponse(response=response)
|
|
415
|
+
json_response = await asyncio.shield(self._get_json_reponse(response=response))
|
|
412
416
|
|
|
413
417
|
if error := json_response[_JsonKey.ERROR]:
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
418
|
+
# Map JSON-RPC error to actionable exception with context
|
|
419
|
+
ctx = RpcContext(protocol="json-rpc", method=str(method), host=self._url)
|
|
420
|
+
exc = map_jsonrpc_error(error=error, ctx=ctx)
|
|
421
|
+
# Structured boundary log at warning level (recoverable per-call failure)
|
|
422
|
+
log_boundary_error(
|
|
423
|
+
logger=_LOGGER,
|
|
424
|
+
boundary="json-rpc",
|
|
425
|
+
action=str(method),
|
|
426
|
+
err=exc,
|
|
427
|
+
level=logging.WARNING,
|
|
428
|
+
context={"url": self._url},
|
|
429
|
+
)
|
|
430
|
+
_LOGGER.debug("POST: %s", exc)
|
|
431
|
+
raise exc
|
|
425
432
|
|
|
426
433
|
return json_response
|
|
427
434
|
|
|
428
435
|
message = f"Status: {response.status}"
|
|
429
|
-
json_response = await self._get_json_reponse(response=response)
|
|
436
|
+
json_response = await asyncio.shield(self._get_json_reponse(response=response))
|
|
430
437
|
if error := json_response[_JsonKey.ERROR]:
|
|
431
|
-
|
|
432
|
-
|
|
438
|
+
ctx = RpcContext(protocol="json-rpc", method=str(method), host=self._url)
|
|
439
|
+
exc = map_jsonrpc_error(error=error, ctx=ctx)
|
|
440
|
+
log_boundary_error(
|
|
441
|
+
logger=_LOGGER,
|
|
442
|
+
boundary="json-rpc",
|
|
443
|
+
action=str(method),
|
|
444
|
+
err=exc,
|
|
445
|
+
level=logging.WARNING,
|
|
446
|
+
context={"url": self._url, "status": response.status},
|
|
447
|
+
)
|
|
448
|
+
raise exc
|
|
433
449
|
raise ClientException(message)
|
|
434
|
-
except BaseHomematicException:
|
|
450
|
+
except BaseHomematicException as bhe:
|
|
435
451
|
if method in (_JsonRpcMethod.SESSION_LOGIN, _JsonRpcMethod.SESSION_LOGOUT, _JsonRpcMethod.SESSION_RENEW):
|
|
436
452
|
self.clear_session()
|
|
453
|
+
# Domain error at boundary -> warning
|
|
454
|
+
log_boundary_error(
|
|
455
|
+
logger=_LOGGER,
|
|
456
|
+
boundary="json-rpc",
|
|
457
|
+
action=str(method),
|
|
458
|
+
err=bhe,
|
|
459
|
+
level=logging.WARNING,
|
|
460
|
+
context={"url": self._url},
|
|
461
|
+
)
|
|
437
462
|
raise
|
|
438
463
|
except ClientConnectorCertificateError as cccerr:
|
|
439
464
|
self.clear_session()
|
|
@@ -443,12 +468,36 @@ class JsonRpcAioHttpClient:
|
|
|
443
468
|
f"{message}. Possible reason: 'Automatic forwarding to HTTPS' is enabled in backend, "
|
|
444
469
|
f"but this integration is not configured to use TLS"
|
|
445
470
|
)
|
|
471
|
+
log_boundary_error(
|
|
472
|
+
logger=_LOGGER,
|
|
473
|
+
boundary="json-rpc",
|
|
474
|
+
action=str(method),
|
|
475
|
+
err=cccerr,
|
|
476
|
+
level=logging.ERROR,
|
|
477
|
+
context={"url": self._url},
|
|
478
|
+
)
|
|
446
479
|
raise ClientException(message) from cccerr
|
|
447
480
|
except (ClientError, OSError) as err:
|
|
448
481
|
self.clear_session()
|
|
482
|
+
log_boundary_error(
|
|
483
|
+
logger=_LOGGER,
|
|
484
|
+
boundary="json-rpc",
|
|
485
|
+
action=str(method),
|
|
486
|
+
err=err,
|
|
487
|
+
level=logging.ERROR,
|
|
488
|
+
context={"url": self._url},
|
|
489
|
+
)
|
|
449
490
|
raise NoConnectionException(err) from err
|
|
450
491
|
except (TypeError, Exception) as exc:
|
|
451
492
|
self.clear_session()
|
|
493
|
+
log_boundary_error(
|
|
494
|
+
logger=_LOGGER,
|
|
495
|
+
boundary="json-rpc",
|
|
496
|
+
action=str(method),
|
|
497
|
+
err=exc,
|
|
498
|
+
level=logging.ERROR,
|
|
499
|
+
context={"url": self._url},
|
|
500
|
+
)
|
|
452
501
|
raise ClientException(exc) from exc
|
|
453
502
|
|
|
454
503
|
async def _get_json_reponse(self, response: ClientResponse) -> dict[str, Any] | Any:
|
aiohomematic/client/xml_rpc.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
1
3
|
"""
|
|
2
4
|
XML-RPC transport proxy with concurrency control and connection awareness.
|
|
3
5
|
|
|
@@ -19,6 +21,7 @@ Notes
|
|
|
19
21
|
|
|
20
22
|
from __future__ import annotations
|
|
21
23
|
|
|
24
|
+
import asyncio
|
|
22
25
|
from collections.abc import Mapping
|
|
23
26
|
from concurrent.futures import ThreadPoolExecutor
|
|
24
27
|
from enum import Enum, IntEnum, StrEnum
|
|
@@ -30,6 +33,7 @@ import xmlrpc.client
|
|
|
30
33
|
|
|
31
34
|
from aiohomematic import central as hmcu
|
|
32
35
|
from aiohomematic.async_support import Looper
|
|
36
|
+
from aiohomematic.client._rpc_errors import RpcContext, map_xmlrpc_fault
|
|
33
37
|
from aiohomematic.const import ISO_8859_1
|
|
34
38
|
from aiohomematic.exceptions import (
|
|
35
39
|
AuthFailure,
|
|
@@ -134,13 +138,15 @@ class XmlRpcProxy(xmlrpc.client.ServerProxy):
|
|
|
134
138
|
):
|
|
135
139
|
args = _cleanup_args(*args)
|
|
136
140
|
_LOGGER.debug("__ASYNC_REQUEST: %s", args)
|
|
137
|
-
result = await
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
result = await asyncio.shield(
|
|
142
|
+
self._looper.async_add_executor_job(
|
|
143
|
+
# pylint: disable=protected-access
|
|
144
|
+
parent._ServerProxy__request, # type: ignore[attr-defined]
|
|
145
|
+
self,
|
|
146
|
+
*args,
|
|
147
|
+
name="xmp_rpc_proxy",
|
|
148
|
+
executor=self._proxy_executor,
|
|
149
|
+
)
|
|
144
150
|
)
|
|
145
151
|
self._connection_state.remove_issue(issuer=self, iid=self.interface_id)
|
|
146
152
|
return result
|
|
@@ -165,7 +171,8 @@ class XmlRpcProxy(xmlrpc.client.ServerProxy):
|
|
|
165
171
|
_LOGGER.error(message)
|
|
166
172
|
raise NoConnectionException(message) from oserr
|
|
167
173
|
except xmlrpc.client.Fault as flt:
|
|
168
|
-
|
|
174
|
+
ctx = RpcContext(protocol="xml-rpc", method=str(args[0]), interface=self.interface_id)
|
|
175
|
+
raise map_xmlrpc_fault(code=flt.faultCode, fault_string=flt.faultString, ctx=ctx) from flt
|
|
169
176
|
except TypeError as terr:
|
|
170
177
|
raise ClientException(terr) from terr
|
|
171
178
|
except xmlrpc.client.ProtocolError as perr:
|
aiohomematic/const.py
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
3
|
+
"""
|
|
4
|
+
Constants used by aiohomematic.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
from __future__ import annotations
|
|
4
10
|
|
|
@@ -6,12 +12,14 @@ from collections.abc import Callable, Iterable, Mapping
|
|
|
6
12
|
from dataclasses import dataclass, field
|
|
7
13
|
from datetime import datetime
|
|
8
14
|
from enum import Enum, IntEnum, StrEnum
|
|
15
|
+
import inspect
|
|
9
16
|
import os
|
|
10
17
|
import re
|
|
11
18
|
import sys
|
|
12
|
-
from
|
|
19
|
+
from types import MappingProxyType
|
|
20
|
+
from typing import Any, Final, NamedTuple, Required, TypeAlias, TypedDict
|
|
13
21
|
|
|
14
|
-
VERSION: Final = "2025.8.
|
|
22
|
+
VERSION: Final = "2025.8.10"
|
|
15
23
|
|
|
16
24
|
# Detect test speedup mode via environment
|
|
17
25
|
_TEST_SPEEDUP: Final = (
|
|
@@ -24,7 +32,7 @@ DEFAULT_ENABLE_DEVICE_FIRMWARE_CHECK: Final = False
|
|
|
24
32
|
DEFAULT_ENABLE_PROGRAM_SCAN: Final = True
|
|
25
33
|
DEFAULT_ENABLE_SYSVAR_SCAN: Final = True
|
|
26
34
|
DEFAULT_HM_MASTER_POLL_AFTER_SEND_INTERVALS: Final = (5,)
|
|
27
|
-
DEFAULT_IGNORE_CUSTOM_DEVICE_DEFINITION_MODELS: Final[
|
|
35
|
+
DEFAULT_IGNORE_CUSTOM_DEVICE_DEFINITION_MODELS: Final[frozenset[str]] = frozenset()
|
|
28
36
|
DEFAULT_INCLUDE_INTERNAL_PROGRAMS: Final = False
|
|
29
37
|
DEFAULT_INCLUDE_INTERNAL_SYSVARS: Final = True
|
|
30
38
|
DEFAULT_MAX_READ_WORKERS: Final = 1
|
|
@@ -35,7 +43,7 @@ DEFAULT_PROGRAM_MARKERS: Final[tuple[DescriptionMarker | str, ...]] = ()
|
|
|
35
43
|
DEFAULT_SYSVAR_MARKERS: Final[tuple[DescriptionMarker | str, ...]] = ()
|
|
36
44
|
DEFAULT_SYS_SCAN_INTERVAL: Final = 30
|
|
37
45
|
DEFAULT_TLS: Final = False
|
|
38
|
-
DEFAULT_UN_IGNORES: Final[
|
|
46
|
+
DEFAULT_UN_IGNORES: Final[frozenset[str]] = frozenset()
|
|
39
47
|
DEFAULT_VERIFY_TLS: Final = False
|
|
40
48
|
|
|
41
49
|
# Default encoding for json service calls, persistent cache
|
|
@@ -52,22 +60,25 @@ CHANNEL_ADDRESS_PATTERN: Final = re.compile(r"^[0-9a-zA-Z-]{5,20}:[0-9]{1,3}$")
|
|
|
52
60
|
DEVICE_ADDRESS_PATTERN: Final = re.compile(r"^[0-9a-zA-Z-]{5,20}$")
|
|
53
61
|
ALLOWED_HOSTNAME_PATTERN: Final = re.compile(r"(?!-)[a-z0-9-]{1,63}(?<!-)$", re.IGNORECASE)
|
|
54
62
|
HTMLTAG_PATTERN: Final = re.compile(r"<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});")
|
|
55
|
-
SCHEDULER_PROFILE_PATTERN = re.compile(
|
|
63
|
+
SCHEDULER_PROFILE_PATTERN: Final = re.compile(
|
|
56
64
|
r"^P[1-6]_(ENDTIME|TEMPERATURE)_(MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FRIDAY|SATURDAY|SUNDAY)_([1-9]|1[0-3])$"
|
|
57
65
|
)
|
|
58
|
-
SCHEDULER_TIME_PATTERN = re.compile(r"^(([0-1]{0,1}[0-9])|(2[0-4])):[0-5][0-9]")
|
|
59
|
-
|
|
60
|
-
ALWAYS_ENABLE_SYSVARS_BY_ID: Final = "40", "41"
|
|
61
|
-
RENAME_SYSVAR_BY_NAME: Final =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
+
SCHEDULER_TIME_PATTERN: Final = re.compile(r"^(([0-1]{0,1}[0-9])|(2[0-4])):[0-5][0-9]")
|
|
67
|
+
|
|
68
|
+
ALWAYS_ENABLE_SYSVARS_BY_ID: Final[frozenset[str]] = frozenset({"40", "41"})
|
|
69
|
+
RENAME_SYSVAR_BY_NAME: Final[Mapping[str, str]] = MappingProxyType(
|
|
70
|
+
{
|
|
71
|
+
"${sysVarAlarmMessages}": "ALARM_MESSAGES",
|
|
72
|
+
"${sysVarPresence}": "PRESENCE",
|
|
73
|
+
"${sysVarServiceMessages}": "SERVICE_MESSAGES",
|
|
74
|
+
}
|
|
75
|
+
)
|
|
66
76
|
|
|
67
|
-
|
|
77
|
+
# Deprecated alias (use ALWAYS_ENABLE_SYSVARS_BY_ID). Kept for backward compatibility.
|
|
78
|
+
SYSVAR_ENABLE_DEFAULT: Final[frozenset[str]] = ALWAYS_ENABLE_SYSVARS_BY_ID
|
|
68
79
|
|
|
69
80
|
ADDRESS_SEPARATOR: Final = ":"
|
|
70
|
-
BLOCK_LOG_TIMEOUT = 60
|
|
81
|
+
BLOCK_LOG_TIMEOUT: Final = 60
|
|
71
82
|
CACHE_PATH: Final = "cache"
|
|
72
83
|
CONF_PASSWORD: Final = "password"
|
|
73
84
|
CONF_USERNAME: Final = "username"
|
|
@@ -79,7 +90,7 @@ DEVICE_DESCRIPTIONS_DIR: Final = "export_device_descriptions"
|
|
|
79
90
|
DEVICE_FIRMWARE_CHECK_INTERVAL: Final = 21600 # 6h
|
|
80
91
|
DEVICE_FIRMWARE_DELIVERING_CHECK_INTERVAL: Final = 3600 # 1h
|
|
81
92
|
DEVICE_FIRMWARE_UPDATING_CHECK_INTERVAL: Final = 300 # 5m
|
|
82
|
-
DUMMY_SERIAL = "SN0815"
|
|
93
|
+
DUMMY_SERIAL: Final = "SN0815"
|
|
83
94
|
FILE_DEVICES: Final = "homematic_devices.json"
|
|
84
95
|
FILE_PARAMSETS: Final = "homematic_paramsets.json"
|
|
85
96
|
HUB_PATH: Final = "hub"
|
|
@@ -87,7 +98,7 @@ IDENTIFIER_SEPARATOR: Final = "@"
|
|
|
87
98
|
INIT_DATETIME: Final = datetime.strptime("01.01.1970 00:00:00", DATETIME_FORMAT)
|
|
88
99
|
IP_ANY_V4: Final = "0.0.0.0"
|
|
89
100
|
JSON_SESSION_AGE: Final = 90
|
|
90
|
-
KWARGS_ARG_DATA_POINT = "data_point"
|
|
101
|
+
KWARGS_ARG_DATA_POINT: Final = "data_point"
|
|
91
102
|
LAST_COMMAND_SEND_STORE_TIMEOUT: Final = 60
|
|
92
103
|
LOCAL_HOST: Final = "127.0.0.1"
|
|
93
104
|
MAX_CACHE_AGE: Final = 10
|
|
@@ -113,7 +124,7 @@ WAIT_FOR_CALLBACK: Final[int | None] = None
|
|
|
113
124
|
SCHEDULER_NOT_STARTED_SLEEP: Final = 0.05 if _TEST_SPEEDUP else 10
|
|
114
125
|
SCHEDULER_LOOP_SLEEP: Final = 0.05 if _TEST_SPEEDUP else 5
|
|
115
126
|
|
|
116
|
-
CALLBACK_WARN_INTERVAL = CONNECTION_CHECKER_INTERVAL * 40
|
|
127
|
+
CALLBACK_WARN_INTERVAL: Final = CONNECTION_CHECKER_INTERVAL * 40
|
|
117
128
|
|
|
118
129
|
# Path
|
|
119
130
|
PROGRAM_SET_PATH_ROOT: Final = "program/set"
|
|
@@ -125,7 +136,7 @@ SYSVAR_STATE_PATH_ROOT: Final = "sysvar/status"
|
|
|
125
136
|
VIRTDEV_SET_PATH_ROOT: Final = "virtdev/set"
|
|
126
137
|
VIRTDEV_STATE_PATH_ROOT: Final = "virtdev/status"
|
|
127
138
|
|
|
128
|
-
CALLBACK_TYPE = Callable[[], None] | None
|
|
139
|
+
CALLBACK_TYPE: TypeAlias = Callable[[], None] | None
|
|
129
140
|
|
|
130
141
|
|
|
131
142
|
class Backend(StrEnum):
|
|
@@ -158,6 +169,17 @@ class CallSource(StrEnum):
|
|
|
158
169
|
MANUAL_OR_SCHEDULED = "manual_or_scheduled"
|
|
159
170
|
|
|
160
171
|
|
|
172
|
+
class CentralUnitState(StrEnum):
|
|
173
|
+
"""Enum with central unit states."""
|
|
174
|
+
|
|
175
|
+
INITIALIZING = "initializing"
|
|
176
|
+
NEW = "new"
|
|
177
|
+
RUNNING = "running"
|
|
178
|
+
STOPPED = "stopped"
|
|
179
|
+
STOPPED_BY_ERROR = "stopped_by_error"
|
|
180
|
+
STOPPING = "stopping"
|
|
181
|
+
|
|
182
|
+
|
|
161
183
|
class DataOperationResult(Enum):
|
|
162
184
|
"""Enum with data operation results."""
|
|
163
185
|
|
|
@@ -536,22 +558,26 @@ class ParameterType(StrEnum):
|
|
|
536
558
|
EMPTY = ""
|
|
537
559
|
|
|
538
560
|
|
|
539
|
-
CLICK_EVENTS: Final[
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
561
|
+
CLICK_EVENTS: Final[frozenset[Parameter]] = frozenset(
|
|
562
|
+
{
|
|
563
|
+
Parameter.PRESS,
|
|
564
|
+
Parameter.PRESS_CONT,
|
|
565
|
+
Parameter.PRESS_LOCK,
|
|
566
|
+
Parameter.PRESS_LONG,
|
|
567
|
+
Parameter.PRESS_LONG_RELEASE,
|
|
568
|
+
Parameter.PRESS_LONG_START,
|
|
569
|
+
Parameter.PRESS_SHORT,
|
|
570
|
+
Parameter.PRESS_UNLOCK,
|
|
571
|
+
}
|
|
548
572
|
)
|
|
549
573
|
|
|
550
574
|
DEVICE_ERROR_EVENTS: Final[tuple[Parameter, ...]] = (Parameter.ERROR, Parameter.SENSOR_ERROR)
|
|
551
575
|
|
|
552
|
-
DATA_POINT_EVENTS: Final[
|
|
553
|
-
|
|
554
|
-
|
|
576
|
+
DATA_POINT_EVENTS: Final[frozenset[EventType]] = frozenset(
|
|
577
|
+
{
|
|
578
|
+
EventType.IMPULSE,
|
|
579
|
+
EventType.KEYPRESS,
|
|
580
|
+
}
|
|
555
581
|
)
|
|
556
582
|
|
|
557
583
|
|
|
@@ -567,26 +593,34 @@ class DataPointKey(NamedTuple):
|
|
|
567
593
|
type DP_KEY_VALUE = tuple[DataPointKey, Any]
|
|
568
594
|
type SYSVAR_TYPE = bool | float | int | str | None
|
|
569
595
|
|
|
570
|
-
HMIP_FIRMWARE_UPDATE_IN_PROGRESS_STATES: Final[
|
|
571
|
-
|
|
572
|
-
|
|
596
|
+
HMIP_FIRMWARE_UPDATE_IN_PROGRESS_STATES: Final[frozenset[DeviceFirmwareState]] = frozenset(
|
|
597
|
+
{
|
|
598
|
+
DeviceFirmwareState.DO_UPDATE_PENDING,
|
|
599
|
+
DeviceFirmwareState.PERFORMING_UPDATE,
|
|
600
|
+
}
|
|
573
601
|
)
|
|
574
602
|
|
|
575
|
-
HMIP_FIRMWARE_UPDATE_READY_STATES: Final[
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
603
|
+
HMIP_FIRMWARE_UPDATE_READY_STATES: Final[frozenset[DeviceFirmwareState]] = frozenset(
|
|
604
|
+
{
|
|
605
|
+
DeviceFirmwareState.READY_FOR_UPDATE,
|
|
606
|
+
DeviceFirmwareState.DO_UPDATE_PENDING,
|
|
607
|
+
DeviceFirmwareState.PERFORMING_UPDATE,
|
|
608
|
+
}
|
|
579
609
|
)
|
|
580
610
|
|
|
581
|
-
IMPULSE_EVENTS: Final[
|
|
611
|
+
IMPULSE_EVENTS: Final[frozenset[Parameter]] = frozenset({Parameter.SEQUENCE_OK})
|
|
582
612
|
|
|
583
|
-
KEY_CHANNEL_OPERATION_MODE_VISIBILITY: Final[Mapping[str,
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
613
|
+
KEY_CHANNEL_OPERATION_MODE_VISIBILITY: Final[Mapping[str, frozenset[str]]] = MappingProxyType(
|
|
614
|
+
{
|
|
615
|
+
Parameter.STATE: frozenset({"BINARY_BEHAVIOR"}),
|
|
616
|
+
Parameter.PRESS_LONG: frozenset({"KEY_BEHAVIOR", "SWITCH_BEHAVIOR"}),
|
|
617
|
+
Parameter.PRESS_LONG_RELEASE: frozenset({"KEY_BEHAVIOR", "SWITCH_BEHAVIOR"}),
|
|
618
|
+
Parameter.PRESS_LONG_START: frozenset({"KEY_BEHAVIOR", "SWITCH_BEHAVIOR"}),
|
|
619
|
+
Parameter.PRESS_SHORT: frozenset({"KEY_BEHAVIOR", "SWITCH_BEHAVIOR"}),
|
|
620
|
+
}
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
BLOCKED_CATEGORIES: Final[tuple[DataPointCategory, ...]] = (DataPointCategory.ACTION,)
|
|
590
624
|
|
|
591
625
|
HUB_CATEGORIES: Final[tuple[DataPointCategory, ...]] = (
|
|
592
626
|
DataPointCategory.HUB_BINARY_SENSOR,
|
|
@@ -616,42 +650,54 @@ CATEGORIES: Final[tuple[DataPointCategory, ...]] = (
|
|
|
616
650
|
DataPointCategory.VALVE,
|
|
617
651
|
)
|
|
618
652
|
|
|
619
|
-
PRIMARY_CLIENT_CANDIDATE_INTERFACES: Final = (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
653
|
+
PRIMARY_CLIENT_CANDIDATE_INTERFACES: Final[frozenset[Interface]] = frozenset(
|
|
654
|
+
{
|
|
655
|
+
Interface.HMIP_RF,
|
|
656
|
+
Interface.BIDCOS_RF,
|
|
657
|
+
Interface.BIDCOS_WIRED,
|
|
658
|
+
}
|
|
623
659
|
)
|
|
624
660
|
|
|
625
|
-
RELEVANT_INIT_PARAMETERS: Final[
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
661
|
+
RELEVANT_INIT_PARAMETERS: Final[frozenset[Parameter]] = frozenset(
|
|
662
|
+
{
|
|
663
|
+
Parameter.CONFIG_PENDING,
|
|
664
|
+
Parameter.STICKY_UN_REACH,
|
|
665
|
+
Parameter.UN_REACH,
|
|
666
|
+
}
|
|
629
667
|
)
|
|
630
668
|
|
|
631
|
-
INTERFACES_SUPPORTING_FIRMWARE_UPDATES: Final[
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
669
|
+
INTERFACES_SUPPORTING_FIRMWARE_UPDATES: Final[frozenset[Interface]] = frozenset(
|
|
670
|
+
{
|
|
671
|
+
Interface.BIDCOS_RF,
|
|
672
|
+
Interface.BIDCOS_WIRED,
|
|
673
|
+
Interface.HMIP_RF,
|
|
674
|
+
}
|
|
635
675
|
)
|
|
636
676
|
|
|
637
|
-
INTERFACES_SUPPORTING_XML_RPC: Final[
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
677
|
+
INTERFACES_SUPPORTING_XML_RPC: Final[frozenset[Interface]] = frozenset(
|
|
678
|
+
{
|
|
679
|
+
Interface.BIDCOS_RF,
|
|
680
|
+
Interface.BIDCOS_WIRED,
|
|
681
|
+
Interface.HMIP_RF,
|
|
682
|
+
Interface.VIRTUAL_DEVICES,
|
|
683
|
+
}
|
|
642
684
|
)
|
|
643
685
|
|
|
644
|
-
INTERFACES_REQUIRING_PERIODIC_REFRESH: Final[
|
|
645
|
-
|
|
646
|
-
|
|
686
|
+
INTERFACES_REQUIRING_PERIODIC_REFRESH: Final[frozenset[Interface]] = frozenset(
|
|
687
|
+
{
|
|
688
|
+
Interface.CCU_JACK,
|
|
689
|
+
Interface.CUXD,
|
|
690
|
+
}
|
|
647
691
|
)
|
|
648
692
|
|
|
649
693
|
DEFAULT_USE_PERIODIC_SCAN_FOR_INTERFACES: Final = True
|
|
650
694
|
|
|
651
|
-
IGNORE_FOR_UN_IGNORE_PARAMETERS: Final[
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
695
|
+
IGNORE_FOR_UN_IGNORE_PARAMETERS: Final[frozenset[Parameter]] = frozenset(
|
|
696
|
+
{
|
|
697
|
+
Parameter.CONFIG_PENDING,
|
|
698
|
+
Parameter.STICKY_UN_REACH,
|
|
699
|
+
Parameter.UN_REACH,
|
|
700
|
+
}
|
|
655
701
|
)
|
|
656
702
|
|
|
657
703
|
|
|
@@ -659,12 +705,14 @@ IGNORE_FOR_UN_IGNORE_PARAMETERS: Final[tuple[Parameter, ...]] = (
|
|
|
659
705
|
_IGNORE_ON_INITIAL_LOAD_PARAMETERS_END_RE: Final = re.compile(r".*(_ERROR)$")
|
|
660
706
|
# Ignore Parameter on initial load that start with
|
|
661
707
|
_IGNORE_ON_INITIAL_LOAD_PARAMETERS_START_RE: Final = re.compile(r"^(ERROR_|RSSI_)")
|
|
662
|
-
_IGNORE_ON_INITIAL_LOAD_PARAMETERS: Final = (
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
708
|
+
_IGNORE_ON_INITIAL_LOAD_PARAMETERS: Final[frozenset[Parameter]] = frozenset(
|
|
709
|
+
{
|
|
710
|
+
Parameter.DUTY_CYCLE,
|
|
711
|
+
Parameter.DUTYCYCLE,
|
|
712
|
+
Parameter.LOW_BAT,
|
|
713
|
+
Parameter.LOWBAT,
|
|
714
|
+
Parameter.OPERATING_VOLTAGE,
|
|
715
|
+
}
|
|
668
716
|
)
|
|
669
717
|
|
|
670
718
|
|
|
@@ -793,3 +841,23 @@ class DeviceDescription(TypedDict, total=False):
|
|
|
793
841
|
INTERFACE: str | None
|
|
794
842
|
# ROAMING: int | None
|
|
795
843
|
RX_MODE: int
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
# Define public API for this module
|
|
847
|
+
__all__ = tuple(
|
|
848
|
+
sorted(
|
|
849
|
+
name
|
|
850
|
+
for name, obj in globals().items()
|
|
851
|
+
if not name.startswith("_")
|
|
852
|
+
and (
|
|
853
|
+
name.isupper() # constants like VERSION, patterns, defaults
|
|
854
|
+
or inspect.isclass(obj) # Enums, dataclasses, TypedDicts, NamedTuple classes
|
|
855
|
+
or inspect.isfunction(obj) # module functions
|
|
856
|
+
)
|
|
857
|
+
and (
|
|
858
|
+
getattr(obj, "__module__", __name__) == __name__
|
|
859
|
+
if not isinstance(obj, (int, float, str, bytes, tuple, frozenset, dict))
|
|
860
|
+
else True
|
|
861
|
+
)
|
|
862
|
+
)
|
|
863
|
+
)
|
aiohomematic/context.py
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
3
|
+
"""
|
|
4
|
+
Collection of context variables.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
from __future__ import annotations
|
|
4
10
|
|
|
@@ -6,3 +12,7 @@ from contextvars import ContextVar
|
|
|
6
12
|
|
|
7
13
|
# context var for storing if call is running within a service
|
|
8
14
|
IN_SERVICE_VAR: ContextVar[bool] = ContextVar("in_service_var", default=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Define public API for this module
|
|
18
|
+
__all__ = ["IN_SERVICE_VAR"]
|
aiohomematic/converter.py
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
3
|
+
"""
|
|
4
|
+
Converters used by aiohomematic.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
from __future__ import annotations
|
|
4
10
|
|
|
5
11
|
import ast
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
import inspect
|
|
6
14
|
import logging
|
|
7
15
|
from typing import Any, Final, cast
|
|
8
16
|
|
|
@@ -12,6 +20,7 @@ from aiohomematic.support import extract_exc_args
|
|
|
12
20
|
_LOGGER = logging.getLogger(__name__)
|
|
13
21
|
|
|
14
22
|
|
|
23
|
+
@lru_cache(maxsize=1024)
|
|
15
24
|
def _convert_cpv_to_hm_level(cpv: Any) -> Any:
|
|
16
25
|
"""Convert combined parameter value for hm level."""
|
|
17
26
|
if isinstance(cpv, str) and cpv.startswith("0x"):
|
|
@@ -19,11 +28,13 @@ def _convert_cpv_to_hm_level(cpv: Any) -> Any:
|
|
|
19
28
|
return cpv
|
|
20
29
|
|
|
21
30
|
|
|
31
|
+
@lru_cache(maxsize=1024)
|
|
22
32
|
def _convert_cpv_to_hmip_level(cpv: Any) -> Any:
|
|
23
33
|
"""Convert combined parameter value for hmip level."""
|
|
24
34
|
return int(cpv) / 100
|
|
25
35
|
|
|
26
36
|
|
|
37
|
+
@lru_cache(maxsize=1024)
|
|
27
38
|
def convert_hm_level_to_cpv(hm_level: Any) -> Any:
|
|
28
39
|
"""Convert hm level to combined parameter value."""
|
|
29
40
|
return format(int(hm_level * 100 * 2), "#04x")
|
|
@@ -40,6 +51,7 @@ _COMBINED_PARAMETER_TO_HM_CONVERTER: Final = {
|
|
|
40
51
|
_COMBINED_PARAMETER_NAMES: Final = {"L": Parameter.LEVEL, "L2": Parameter.LEVEL_2}
|
|
41
52
|
|
|
42
53
|
|
|
54
|
+
@lru_cache(maxsize=1024)
|
|
43
55
|
def _convert_combined_parameter_to_paramset(cpv: str) -> dict[str, Any]:
|
|
44
56
|
"""Convert combined parameter to paramset."""
|
|
45
57
|
paramset: dict[str, Any] = {}
|
|
@@ -53,6 +65,7 @@ def _convert_combined_parameter_to_paramset(cpv: str) -> dict[str, Any]:
|
|
|
53
65
|
return paramset
|
|
54
66
|
|
|
55
67
|
|
|
68
|
+
@lru_cache(maxsize=1024)
|
|
56
69
|
def _convert_level_combined_to_paramset(lcv: str) -> dict[str, Any]:
|
|
57
70
|
"""Convert combined parameter to paramset."""
|
|
58
71
|
if "," in lcv:
|
|
@@ -71,6 +84,7 @@ _COMBINED_PARAMETER_TO_PARAMSET_CONVERTER: Final = {
|
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
|
|
87
|
+
@lru_cache(maxsize=1024)
|
|
74
88
|
def convert_combined_parameter_to_paramset(parameter: str, cpv: str) -> dict[str, Any]:
|
|
75
89
|
"""Convert combined parameter to paramset."""
|
|
76
90
|
try:
|
|
@@ -80,3 +94,15 @@ def convert_combined_parameter_to_paramset(parameter: str, cpv: str) -> dict[str
|
|
|
80
94
|
except Exception as exc:
|
|
81
95
|
_LOGGER.debug("CONVERT_COMBINED_PARAMETER_TO_PARAMSET: Convert failed %s", extract_exc_args(exc=exc))
|
|
82
96
|
return {}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Define public API for this module
|
|
100
|
+
__all__ = tuple(
|
|
101
|
+
sorted(
|
|
102
|
+
name
|
|
103
|
+
for name, obj in globals().items()
|
|
104
|
+
if not name.startswith("_")
|
|
105
|
+
and (name.isupper() or inspect.isfunction(obj) or inspect.isclass(obj))
|
|
106
|
+
and getattr(obj, "__module__", __name__) == __name__
|
|
107
|
+
)
|
|
108
|
+
)
|