aiohomematic 2025.8.8__py3-none-any.whl → 2025.8.9__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/caches/persistent.py +27 -22
- aiohomematic/caches/visibility.py +275 -252
- aiohomematic/central/__init__.py +26 -31
- aiohomematic/const.py +103 -76
- aiohomematic/model/calculated/data_point.py +5 -1
- aiohomematic/model/custom/data_point.py +4 -0
- aiohomematic/model/data_point.py +15 -0
- aiohomematic/model/device.py +7 -5
- aiohomematic/model/hub/data_point.py +4 -0
- aiohomematic/model/update.py +4 -0
- aiohomematic/support.py +15 -2
- {aiohomematic-2025.8.8.dist-info → aiohomematic-2025.8.9.dist-info}/METADATA +1 -1
- {aiohomematic-2025.8.8.dist-info → aiohomematic-2025.8.9.dist-info}/RECORD +16 -16
- {aiohomematic-2025.8.8.dist-info → aiohomematic-2025.8.9.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.8.8.dist-info → aiohomematic-2025.8.9.dist-info}/licenses/LICENSE +0 -0
- {aiohomematic-2025.8.8.dist-info → aiohomematic-2025.8.9.dist-info}/top_level.txt +0 -0
aiohomematic/central/__init__.py
CHANGED
|
@@ -298,14 +298,14 @@ class CentralUnit(PayloadMixin):
|
|
|
298
298
|
)
|
|
299
299
|
|
|
300
300
|
@property
|
|
301
|
-
def interface_ids(self) ->
|
|
301
|
+
def interface_ids(self) -> frozenset[str]:
|
|
302
302
|
"""Return all associated interface ids."""
|
|
303
|
-
return
|
|
303
|
+
return frozenset(self._clients)
|
|
304
304
|
|
|
305
305
|
@property
|
|
306
|
-
def interfaces(self) ->
|
|
306
|
+
def interfaces(self) -> frozenset[Interface]:
|
|
307
307
|
"""Return all associated interfaces."""
|
|
308
|
-
return
|
|
308
|
+
return frozenset(client.interface for client in self._clients.values())
|
|
309
309
|
|
|
310
310
|
@property
|
|
311
311
|
def is_alive(self) -> bool:
|
|
@@ -1009,19 +1009,19 @@ class CentralUnit(PayloadMixin):
|
|
|
1009
1009
|
return
|
|
1010
1010
|
|
|
1011
1011
|
async with self._device_add_semaphore:
|
|
1012
|
-
#
|
|
1013
|
-
|
|
1014
|
-
dev_desc["ADDRESS"]
|
|
1015
|
-
for dev_desc in self._device_descriptions.get_raw_device_descriptions(interface_id=interface_id)
|
|
1016
|
-
)
|
|
1012
|
+
# Use mapping membership to avoid rebuilding known addresses and allow O(1) checks.
|
|
1013
|
+
existing_map = self._device_descriptions.get_device_descriptions(interface_id=interface_id)
|
|
1017
1014
|
client = self._clients[interface_id]
|
|
1018
1015
|
save_paramset_descriptions = False
|
|
1019
1016
|
save_device_descriptions = False
|
|
1020
1017
|
for dev_desc in device_descriptions:
|
|
1021
1018
|
try:
|
|
1019
|
+
address = dev_desc["ADDRESS"]
|
|
1020
|
+
# Check existence before mutating cache to ensure we detect truly new addresses.
|
|
1021
|
+
is_new_address = address not in existing_map
|
|
1022
1022
|
self._device_descriptions.add_device(interface_id=interface_id, device_description=dev_desc)
|
|
1023
1023
|
save_device_descriptions = True
|
|
1024
|
-
if
|
|
1024
|
+
if is_new_address:
|
|
1025
1025
|
await client.fetch_paramset_descriptions(device_description=dev_desc)
|
|
1026
1026
|
save_paramset_descriptions = True
|
|
1027
1027
|
except Exception as exc: # pragma: no cover
|
|
@@ -1043,7 +1043,7 @@ class CentralUnit(PayloadMixin):
|
|
|
1043
1043
|
await self._create_devices(new_device_addresses=new_device_addresses)
|
|
1044
1044
|
|
|
1045
1045
|
def _check_for_new_device_addresses(self) -> Mapping[str, set[str]]:
|
|
1046
|
-
"""Check if there are new devices
|
|
1046
|
+
"""Check if there are new devices that need to be created."""
|
|
1047
1047
|
new_device_addresses: dict[str, set[str]] = {}
|
|
1048
1048
|
for interface_id in self.interface_ids:
|
|
1049
1049
|
if not self._paramset_descriptions.has_interface_id(interface_id=interface_id):
|
|
@@ -1053,21 +1053,16 @@ class CentralUnit(PayloadMixin):
|
|
|
1053
1053
|
)
|
|
1054
1054
|
continue
|
|
1055
1055
|
|
|
1056
|
-
if
|
|
1057
|
-
|
|
1058
|
-
|
|
1056
|
+
# Build the set locally and assign only if non-empty to avoid add-then-delete pattern
|
|
1057
|
+
new_set: set[str] = set()
|
|
1059
1058
|
for device_address in self._device_descriptions.get_addresses(interface_id=interface_id):
|
|
1060
1059
|
if device_address not in self._devices:
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
del new_device_addresses[interface_id]
|
|
1060
|
+
new_set.add(device_address)
|
|
1061
|
+
if new_set:
|
|
1062
|
+
new_device_addresses[interface_id] = new_set
|
|
1065
1063
|
|
|
1066
1064
|
if _LOGGER.isEnabledFor(level=DEBUG):
|
|
1067
|
-
count
|
|
1068
|
-
for item in new_device_addresses.values():
|
|
1069
|
-
count += len(item)
|
|
1070
|
-
|
|
1065
|
+
count = sum(len(item) for item in new_device_addresses.values())
|
|
1071
1066
|
_LOGGER.debug(
|
|
1072
1067
|
"CHECK_FOR_NEW_DEVICE_ADDRESSES: %s: %i.",
|
|
1073
1068
|
"Found new device addresses" if new_device_addresses else "Did not find any new device addresses",
|
|
@@ -1298,7 +1293,7 @@ class CentralUnit(PayloadMixin):
|
|
|
1298
1293
|
full_format: bool = False,
|
|
1299
1294
|
un_ignore_candidates_only: bool = False,
|
|
1300
1295
|
use_channel_wildcard: bool = False,
|
|
1301
|
-
) ->
|
|
1296
|
+
) -> tuple[str, ...]:
|
|
1302
1297
|
"""
|
|
1303
1298
|
Return all parameters from VALUES paramset.
|
|
1304
1299
|
|
|
@@ -1368,7 +1363,7 @@ class CentralUnit(PayloadMixin):
|
|
|
1368
1363
|
else:
|
|
1369
1364
|
parameters.add(f"{parameter}:{paramset_key}@{model}:{channel_repr}")
|
|
1370
1365
|
|
|
1371
|
-
return
|
|
1366
|
+
return tuple(parameters)
|
|
1372
1367
|
|
|
1373
1368
|
def _get_virtual_remote(self, device_address: str) -> Device | None:
|
|
1374
1369
|
"""Get the virtual remote for the Client."""
|
|
@@ -1811,8 +1806,8 @@ class CentralConfig:
|
|
|
1811
1806
|
enable_program_scan: bool = DEFAULT_ENABLE_PROGRAM_SCAN,
|
|
1812
1807
|
enable_sysvar_scan: bool = DEFAULT_ENABLE_SYSVAR_SCAN,
|
|
1813
1808
|
hm_master_poll_after_send_intervals: tuple[int, ...] = DEFAULT_HM_MASTER_POLL_AFTER_SEND_INTERVALS,
|
|
1814
|
-
ignore_custom_device_definition_models:
|
|
1815
|
-
interfaces_requiring_periodic_refresh:
|
|
1809
|
+
ignore_custom_device_definition_models: frozenset[str] = DEFAULT_IGNORE_CUSTOM_DEVICE_DEFINITION_MODELS,
|
|
1810
|
+
interfaces_requiring_periodic_refresh: frozenset[Interface] = INTERFACES_REQUIRING_PERIODIC_REFRESH,
|
|
1816
1811
|
json_port: int | None = None,
|
|
1817
1812
|
listen_ip_addr: str | None = None,
|
|
1818
1813
|
listen_port: int | None = None,
|
|
@@ -1823,7 +1818,7 @@ class CentralConfig:
|
|
|
1823
1818
|
sys_scan_interval: int = DEFAULT_SYS_SCAN_INTERVAL,
|
|
1824
1819
|
sysvar_markers: tuple[DescriptionMarker | str, ...] = DEFAULT_SYSVAR_MARKERS,
|
|
1825
1820
|
tls: bool = DEFAULT_TLS,
|
|
1826
|
-
un_ignore_list:
|
|
1821
|
+
un_ignore_list: frozenset[str] = DEFAULT_UN_IGNORES,
|
|
1827
1822
|
verify_tls: bool = DEFAULT_VERIFY_TLS,
|
|
1828
1823
|
) -> None:
|
|
1829
1824
|
"""Init the client config."""
|
|
@@ -1838,8 +1833,8 @@ class CentralConfig:
|
|
|
1838
1833
|
self.enable_sysvar_scan: Final = enable_sysvar_scan
|
|
1839
1834
|
self.hm_master_poll_after_send_intervals: Final = hm_master_poll_after_send_intervals
|
|
1840
1835
|
self.host: Final = host
|
|
1841
|
-
self.ignore_custom_device_definition_models: Final = ignore_custom_device_definition_models
|
|
1842
|
-
self.interfaces_requiring_periodic_refresh: Final = interfaces_requiring_periodic_refresh
|
|
1836
|
+
self.ignore_custom_device_definition_models: Final = frozenset(ignore_custom_device_definition_models or ())
|
|
1837
|
+
self.interfaces_requiring_periodic_refresh: Final = frozenset(interfaces_requiring_periodic_refresh or ())
|
|
1843
1838
|
self.json_port: Final = json_port
|
|
1844
1839
|
self.listen_ip_addr: Final = listen_ip_addr
|
|
1845
1840
|
self.listen_port: Final = listen_port
|
|
@@ -1877,9 +1872,9 @@ class CentralConfig:
|
|
|
1877
1872
|
return 443 if self.tls else 80
|
|
1878
1873
|
|
|
1879
1874
|
@property
|
|
1880
|
-
def enabled_interface_configs(self) ->
|
|
1875
|
+
def enabled_interface_configs(self) -> frozenset[hmcl.InterfaceConfig]:
|
|
1881
1876
|
"""Return the interface configs."""
|
|
1882
|
-
return
|
|
1877
|
+
return frozenset(ic for ic in self._interface_configs if ic.enabled is True)
|
|
1883
1878
|
|
|
1884
1879
|
@property
|
|
1885
1880
|
def use_caches(self) -> bool:
|
aiohomematic/const.py
CHANGED
|
@@ -9,9 +9,10 @@ from enum import Enum, IntEnum, StrEnum
|
|
|
9
9
|
import os
|
|
10
10
|
import re
|
|
11
11
|
import sys
|
|
12
|
-
from
|
|
12
|
+
from types import MappingProxyType
|
|
13
|
+
from typing import Any, Final, NamedTuple, Required, TypeAlias, TypedDict
|
|
13
14
|
|
|
14
|
-
VERSION: Final = "2025.8.
|
|
15
|
+
VERSION: Final = "2025.8.9"
|
|
15
16
|
|
|
16
17
|
# Detect test speedup mode via environment
|
|
17
18
|
_TEST_SPEEDUP: Final = (
|
|
@@ -24,7 +25,7 @@ DEFAULT_ENABLE_DEVICE_FIRMWARE_CHECK: Final = False
|
|
|
24
25
|
DEFAULT_ENABLE_PROGRAM_SCAN: Final = True
|
|
25
26
|
DEFAULT_ENABLE_SYSVAR_SCAN: Final = True
|
|
26
27
|
DEFAULT_HM_MASTER_POLL_AFTER_SEND_INTERVALS: Final = (5,)
|
|
27
|
-
DEFAULT_IGNORE_CUSTOM_DEVICE_DEFINITION_MODELS: Final[
|
|
28
|
+
DEFAULT_IGNORE_CUSTOM_DEVICE_DEFINITION_MODELS: Final[frozenset[str]] = frozenset()
|
|
28
29
|
DEFAULT_INCLUDE_INTERNAL_PROGRAMS: Final = False
|
|
29
30
|
DEFAULT_INCLUDE_INTERNAL_SYSVARS: Final = True
|
|
30
31
|
DEFAULT_MAX_READ_WORKERS: Final = 1
|
|
@@ -35,7 +36,7 @@ DEFAULT_PROGRAM_MARKERS: Final[tuple[DescriptionMarker | str, ...]] = ()
|
|
|
35
36
|
DEFAULT_SYSVAR_MARKERS: Final[tuple[DescriptionMarker | str, ...]] = ()
|
|
36
37
|
DEFAULT_SYS_SCAN_INTERVAL: Final = 30
|
|
37
38
|
DEFAULT_TLS: Final = False
|
|
38
|
-
DEFAULT_UN_IGNORES: Final[
|
|
39
|
+
DEFAULT_UN_IGNORES: Final[frozenset[str]] = frozenset()
|
|
39
40
|
DEFAULT_VERIFY_TLS: Final = False
|
|
40
41
|
|
|
41
42
|
# Default encoding for json service calls, persistent cache
|
|
@@ -52,22 +53,24 @@ CHANNEL_ADDRESS_PATTERN: Final = re.compile(r"^[0-9a-zA-Z-]{5,20}:[0-9]{1,3}$")
|
|
|
52
53
|
DEVICE_ADDRESS_PATTERN: Final = re.compile(r"^[0-9a-zA-Z-]{5,20}$")
|
|
53
54
|
ALLOWED_HOSTNAME_PATTERN: Final = re.compile(r"(?!-)[a-z0-9-]{1,63}(?<!-)$", re.IGNORECASE)
|
|
54
55
|
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(
|
|
56
|
+
SCHEDULER_PROFILE_PATTERN: Final = re.compile(
|
|
56
57
|
r"^P[1-6]_(ENDTIME|TEMPERATURE)_(MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FRIDAY|SATURDAY|SUNDAY)_([1-9]|1[0-3])$"
|
|
57
58
|
)
|
|
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
|
-
}
|
|
59
|
+
SCHEDULER_TIME_PATTERN: Final = re.compile(r"^(([0-1]{0,1}[0-9])|(2[0-4])):[0-5][0-9]")
|
|
60
|
+
|
|
61
|
+
ALWAYS_ENABLE_SYSVARS_BY_ID: Final[frozenset[str]] = frozenset({"40", "41"})
|
|
62
|
+
RENAME_SYSVAR_BY_NAME: Final[Mapping[str, str]] = MappingProxyType(
|
|
63
|
+
{
|
|
64
|
+
"${sysVarAlarmMessages}": "ALARM_MESSAGES",
|
|
65
|
+
"${sysVarPresence}": "PRESENCE",
|
|
66
|
+
"${sysVarServiceMessages}": "SERVICE_MESSAGES",
|
|
67
|
+
}
|
|
68
|
+
)
|
|
66
69
|
|
|
67
|
-
SYSVAR_ENABLE_DEFAULT: Final = "40", "41"
|
|
70
|
+
SYSVAR_ENABLE_DEFAULT: Final[frozenset[str]] = frozenset({"40", "41"})
|
|
68
71
|
|
|
69
72
|
ADDRESS_SEPARATOR: Final = ":"
|
|
70
|
-
BLOCK_LOG_TIMEOUT = 60
|
|
73
|
+
BLOCK_LOG_TIMEOUT: Final = 60
|
|
71
74
|
CACHE_PATH: Final = "cache"
|
|
72
75
|
CONF_PASSWORD: Final = "password"
|
|
73
76
|
CONF_USERNAME: Final = "username"
|
|
@@ -79,7 +82,7 @@ DEVICE_DESCRIPTIONS_DIR: Final = "export_device_descriptions"
|
|
|
79
82
|
DEVICE_FIRMWARE_CHECK_INTERVAL: Final = 21600 # 6h
|
|
80
83
|
DEVICE_FIRMWARE_DELIVERING_CHECK_INTERVAL: Final = 3600 # 1h
|
|
81
84
|
DEVICE_FIRMWARE_UPDATING_CHECK_INTERVAL: Final = 300 # 5m
|
|
82
|
-
DUMMY_SERIAL = "SN0815"
|
|
85
|
+
DUMMY_SERIAL: Final = "SN0815"
|
|
83
86
|
FILE_DEVICES: Final = "homematic_devices.json"
|
|
84
87
|
FILE_PARAMSETS: Final = "homematic_paramsets.json"
|
|
85
88
|
HUB_PATH: Final = "hub"
|
|
@@ -87,7 +90,7 @@ IDENTIFIER_SEPARATOR: Final = "@"
|
|
|
87
90
|
INIT_DATETIME: Final = datetime.strptime("01.01.1970 00:00:00", DATETIME_FORMAT)
|
|
88
91
|
IP_ANY_V4: Final = "0.0.0.0"
|
|
89
92
|
JSON_SESSION_AGE: Final = 90
|
|
90
|
-
KWARGS_ARG_DATA_POINT = "data_point"
|
|
93
|
+
KWARGS_ARG_DATA_POINT: Final = "data_point"
|
|
91
94
|
LAST_COMMAND_SEND_STORE_TIMEOUT: Final = 60
|
|
92
95
|
LOCAL_HOST: Final = "127.0.0.1"
|
|
93
96
|
MAX_CACHE_AGE: Final = 10
|
|
@@ -113,7 +116,7 @@ WAIT_FOR_CALLBACK: Final[int | None] = None
|
|
|
113
116
|
SCHEDULER_NOT_STARTED_SLEEP: Final = 0.05 if _TEST_SPEEDUP else 10
|
|
114
117
|
SCHEDULER_LOOP_SLEEP: Final = 0.05 if _TEST_SPEEDUP else 5
|
|
115
118
|
|
|
116
|
-
CALLBACK_WARN_INTERVAL = CONNECTION_CHECKER_INTERVAL * 40
|
|
119
|
+
CALLBACK_WARN_INTERVAL: Final = CONNECTION_CHECKER_INTERVAL * 40
|
|
117
120
|
|
|
118
121
|
# Path
|
|
119
122
|
PROGRAM_SET_PATH_ROOT: Final = "program/set"
|
|
@@ -125,7 +128,7 @@ SYSVAR_STATE_PATH_ROOT: Final = "sysvar/status"
|
|
|
125
128
|
VIRTDEV_SET_PATH_ROOT: Final = "virtdev/set"
|
|
126
129
|
VIRTDEV_STATE_PATH_ROOT: Final = "virtdev/status"
|
|
127
130
|
|
|
128
|
-
CALLBACK_TYPE = Callable[[], None] | None
|
|
131
|
+
CALLBACK_TYPE: TypeAlias = Callable[[], None] | None
|
|
129
132
|
|
|
130
133
|
|
|
131
134
|
class Backend(StrEnum):
|
|
@@ -536,22 +539,26 @@ class ParameterType(StrEnum):
|
|
|
536
539
|
EMPTY = ""
|
|
537
540
|
|
|
538
541
|
|
|
539
|
-
CLICK_EVENTS: Final[
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
542
|
+
CLICK_EVENTS: Final[frozenset[Parameter]] = frozenset(
|
|
543
|
+
{
|
|
544
|
+
Parameter.PRESS,
|
|
545
|
+
Parameter.PRESS_CONT,
|
|
546
|
+
Parameter.PRESS_LOCK,
|
|
547
|
+
Parameter.PRESS_LONG,
|
|
548
|
+
Parameter.PRESS_LONG_RELEASE,
|
|
549
|
+
Parameter.PRESS_LONG_START,
|
|
550
|
+
Parameter.PRESS_SHORT,
|
|
551
|
+
Parameter.PRESS_UNLOCK,
|
|
552
|
+
}
|
|
548
553
|
)
|
|
549
554
|
|
|
550
555
|
DEVICE_ERROR_EVENTS: Final[tuple[Parameter, ...]] = (Parameter.ERROR, Parameter.SENSOR_ERROR)
|
|
551
556
|
|
|
552
|
-
DATA_POINT_EVENTS: Final[
|
|
553
|
-
|
|
554
|
-
|
|
557
|
+
DATA_POINT_EVENTS: Final[frozenset[EventType]] = frozenset(
|
|
558
|
+
{
|
|
559
|
+
EventType.IMPULSE,
|
|
560
|
+
EventType.KEYPRESS,
|
|
561
|
+
}
|
|
555
562
|
)
|
|
556
563
|
|
|
557
564
|
|
|
@@ -567,26 +574,32 @@ class DataPointKey(NamedTuple):
|
|
|
567
574
|
type DP_KEY_VALUE = tuple[DataPointKey, Any]
|
|
568
575
|
type SYSVAR_TYPE = bool | float | int | str | None
|
|
569
576
|
|
|
570
|
-
HMIP_FIRMWARE_UPDATE_IN_PROGRESS_STATES: Final[
|
|
571
|
-
|
|
572
|
-
|
|
577
|
+
HMIP_FIRMWARE_UPDATE_IN_PROGRESS_STATES: Final[frozenset[DeviceFirmwareState]] = frozenset(
|
|
578
|
+
{
|
|
579
|
+
DeviceFirmwareState.DO_UPDATE_PENDING,
|
|
580
|
+
DeviceFirmwareState.PERFORMING_UPDATE,
|
|
581
|
+
}
|
|
573
582
|
)
|
|
574
583
|
|
|
575
|
-
HMIP_FIRMWARE_UPDATE_READY_STATES: Final[
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
584
|
+
HMIP_FIRMWARE_UPDATE_READY_STATES: Final[frozenset[DeviceFirmwareState]] = frozenset(
|
|
585
|
+
{
|
|
586
|
+
DeviceFirmwareState.READY_FOR_UPDATE,
|
|
587
|
+
DeviceFirmwareState.DO_UPDATE_PENDING,
|
|
588
|
+
DeviceFirmwareState.PERFORMING_UPDATE,
|
|
589
|
+
}
|
|
579
590
|
)
|
|
580
591
|
|
|
581
|
-
IMPULSE_EVENTS: Final[
|
|
592
|
+
IMPULSE_EVENTS: Final[frozenset[Parameter]] = frozenset({Parameter.SEQUENCE_OK})
|
|
582
593
|
|
|
583
|
-
KEY_CHANNEL_OPERATION_MODE_VISIBILITY: Final[Mapping[str,
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
594
|
+
KEY_CHANNEL_OPERATION_MODE_VISIBILITY: Final[Mapping[str, frozenset[str]]] = MappingProxyType(
|
|
595
|
+
{
|
|
596
|
+
Parameter.STATE: frozenset({"BINARY_BEHAVIOR"}),
|
|
597
|
+
Parameter.PRESS_LONG: frozenset({"KEY_BEHAVIOR", "SWITCH_BEHAVIOR"}),
|
|
598
|
+
Parameter.PRESS_LONG_RELEASE: frozenset({"KEY_BEHAVIOR", "SWITCH_BEHAVIOR"}),
|
|
599
|
+
Parameter.PRESS_LONG_START: frozenset({"KEY_BEHAVIOR", "SWITCH_BEHAVIOR"}),
|
|
600
|
+
Parameter.PRESS_SHORT: frozenset({"KEY_BEHAVIOR", "SWITCH_BEHAVIOR"}),
|
|
601
|
+
}
|
|
602
|
+
)
|
|
590
603
|
|
|
591
604
|
HUB_CATEGORIES: Final[tuple[DataPointCategory, ...]] = (
|
|
592
605
|
DataPointCategory.HUB_BINARY_SENSOR,
|
|
@@ -616,42 +629,54 @@ CATEGORIES: Final[tuple[DataPointCategory, ...]] = (
|
|
|
616
629
|
DataPointCategory.VALVE,
|
|
617
630
|
)
|
|
618
631
|
|
|
619
|
-
PRIMARY_CLIENT_CANDIDATE_INTERFACES: Final = (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
632
|
+
PRIMARY_CLIENT_CANDIDATE_INTERFACES: Final[frozenset[Interface]] = frozenset(
|
|
633
|
+
{
|
|
634
|
+
Interface.HMIP_RF,
|
|
635
|
+
Interface.BIDCOS_RF,
|
|
636
|
+
Interface.BIDCOS_WIRED,
|
|
637
|
+
}
|
|
623
638
|
)
|
|
624
639
|
|
|
625
|
-
RELEVANT_INIT_PARAMETERS: Final[
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
640
|
+
RELEVANT_INIT_PARAMETERS: Final[frozenset[Parameter]] = frozenset(
|
|
641
|
+
{
|
|
642
|
+
Parameter.CONFIG_PENDING,
|
|
643
|
+
Parameter.STICKY_UN_REACH,
|
|
644
|
+
Parameter.UN_REACH,
|
|
645
|
+
}
|
|
629
646
|
)
|
|
630
647
|
|
|
631
|
-
INTERFACES_SUPPORTING_FIRMWARE_UPDATES: Final[
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
648
|
+
INTERFACES_SUPPORTING_FIRMWARE_UPDATES: Final[frozenset[Interface]] = frozenset(
|
|
649
|
+
{
|
|
650
|
+
Interface.BIDCOS_RF,
|
|
651
|
+
Interface.BIDCOS_WIRED,
|
|
652
|
+
Interface.HMIP_RF,
|
|
653
|
+
}
|
|
635
654
|
)
|
|
636
655
|
|
|
637
|
-
INTERFACES_SUPPORTING_XML_RPC: Final[
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
656
|
+
INTERFACES_SUPPORTING_XML_RPC: Final[frozenset[Interface]] = frozenset(
|
|
657
|
+
{
|
|
658
|
+
Interface.BIDCOS_RF,
|
|
659
|
+
Interface.BIDCOS_WIRED,
|
|
660
|
+
Interface.HMIP_RF,
|
|
661
|
+
Interface.VIRTUAL_DEVICES,
|
|
662
|
+
}
|
|
642
663
|
)
|
|
643
664
|
|
|
644
|
-
INTERFACES_REQUIRING_PERIODIC_REFRESH: Final[
|
|
645
|
-
|
|
646
|
-
|
|
665
|
+
INTERFACES_REQUIRING_PERIODIC_REFRESH: Final[frozenset[Interface]] = frozenset(
|
|
666
|
+
{
|
|
667
|
+
Interface.CCU_JACK,
|
|
668
|
+
Interface.CUXD,
|
|
669
|
+
}
|
|
647
670
|
)
|
|
648
671
|
|
|
649
672
|
DEFAULT_USE_PERIODIC_SCAN_FOR_INTERFACES: Final = True
|
|
650
673
|
|
|
651
|
-
IGNORE_FOR_UN_IGNORE_PARAMETERS: Final[
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
674
|
+
IGNORE_FOR_UN_IGNORE_PARAMETERS: Final[frozenset[Parameter]] = frozenset(
|
|
675
|
+
{
|
|
676
|
+
Parameter.CONFIG_PENDING,
|
|
677
|
+
Parameter.STICKY_UN_REACH,
|
|
678
|
+
Parameter.UN_REACH,
|
|
679
|
+
}
|
|
655
680
|
)
|
|
656
681
|
|
|
657
682
|
|
|
@@ -659,12 +684,14 @@ IGNORE_FOR_UN_IGNORE_PARAMETERS: Final[tuple[Parameter, ...]] = (
|
|
|
659
684
|
_IGNORE_ON_INITIAL_LOAD_PARAMETERS_END_RE: Final = re.compile(r".*(_ERROR)$")
|
|
660
685
|
# Ignore Parameter on initial load that start with
|
|
661
686
|
_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
|
-
|
|
687
|
+
_IGNORE_ON_INITIAL_LOAD_PARAMETERS: Final[frozenset[Parameter]] = frozenset(
|
|
688
|
+
{
|
|
689
|
+
Parameter.DUTY_CYCLE,
|
|
690
|
+
Parameter.DUTYCYCLE,
|
|
691
|
+
Parameter.LOW_BAT,
|
|
692
|
+
Parameter.LOWBAT,
|
|
693
|
+
Parameter.OPERATING_VOLTAGE,
|
|
694
|
+
}
|
|
668
695
|
)
|
|
669
696
|
|
|
670
697
|
|
|
@@ -280,6 +280,10 @@ class CalculatedDataPoint[ParameterT: GenericParameterType](BaseDataPoint):
|
|
|
280
280
|
"""Generate the usage for the data point."""
|
|
281
281
|
return DataPointUsage.DATA_POINT
|
|
282
282
|
|
|
283
|
+
def _get_signature(self) -> str:
|
|
284
|
+
"""Return the signature of the data_point."""
|
|
285
|
+
return f"{self._category}/{self._channel.device.model}/{self._calculated_parameter}"
|
|
286
|
+
|
|
283
287
|
async def load_data_point_value(self, call_source: CallSource, direct_call: bool = False) -> None:
|
|
284
288
|
"""Init the data point values."""
|
|
285
289
|
for dp in self._readable_data_points:
|
|
@@ -300,7 +304,7 @@ class CalculatedDataPoint[ParameterT: GenericParameterType](BaseDataPoint):
|
|
|
300
304
|
@property
|
|
301
305
|
def _should_fire_data_point_updated_callback(self) -> bool:
|
|
302
306
|
"""Check if a data point has been updated or refreshed."""
|
|
303
|
-
if self.fired_recently:
|
|
307
|
+
if self.fired_recently:
|
|
304
308
|
return False
|
|
305
309
|
|
|
306
310
|
if (relevant_values_data_point := self._relevant_values_data_points) is not None and len(
|
|
@@ -177,6 +177,10 @@ class CustomDataPoint(BaseDataPoint):
|
|
|
177
177
|
return DataPointUsage.CDP_PRIMARY
|
|
178
178
|
return DataPointUsage.CDP_SECONDARY
|
|
179
179
|
|
|
180
|
+
def _get_signature(self) -> str:
|
|
181
|
+
"""Return the signature of the data_point."""
|
|
182
|
+
return f"{self._category}/{self._channel.device.model}/{self.data_point_name_postfix}"
|
|
183
|
+
|
|
180
184
|
async def load_data_point_value(self, call_source: CallSource, direct_call: bool = False) -> None:
|
|
181
185
|
"""Init the data point values."""
|
|
182
186
|
for dp in self._readable_data_points:
|
aiohomematic/model/data_point.py
CHANGED
|
@@ -146,6 +146,7 @@ class CallbackDataPoint(ABC):
|
|
|
146
146
|
"_modified_at",
|
|
147
147
|
"_path_data",
|
|
148
148
|
"_refreshed_at",
|
|
149
|
+
"_signature",
|
|
149
150
|
"_temporary_modified_at",
|
|
150
151
|
"_temporary_refreshed_at",
|
|
151
152
|
"_unique_id",
|
|
@@ -164,6 +165,7 @@ class CallbackDataPoint(ABC):
|
|
|
164
165
|
self._fired_at: datetime = INIT_DATETIME
|
|
165
166
|
self._modified_at: datetime = INIT_DATETIME
|
|
166
167
|
self._refreshed_at: datetime = INIT_DATETIME
|
|
168
|
+
self._signature: Final = self._get_signature()
|
|
167
169
|
self._temporary_modified_at: datetime = INIT_DATETIME
|
|
168
170
|
self._temporary_refreshed_at: datetime = INIT_DATETIME
|
|
169
171
|
|
|
@@ -252,6 +254,11 @@ class CallbackDataPoint(ABC):
|
|
|
252
254
|
def name(self) -> str:
|
|
253
255
|
"""Return the name of the data_point."""
|
|
254
256
|
|
|
257
|
+
@property
|
|
258
|
+
def signature(self) -> str:
|
|
259
|
+
"""Return the data_point signature."""
|
|
260
|
+
return self._signature
|
|
261
|
+
|
|
255
262
|
@config_property
|
|
256
263
|
def unique_id(self) -> str:
|
|
257
264
|
"""Return the unique_id."""
|
|
@@ -325,6 +332,10 @@ class CallbackDataPoint(ABC):
|
|
|
325
332
|
def _get_path_data(self) -> PathData:
|
|
326
333
|
"""Return the path data."""
|
|
327
334
|
|
|
335
|
+
@abstractmethod
|
|
336
|
+
def _get_signature(self) -> str:
|
|
337
|
+
"""Return the signature of the data_point."""
|
|
338
|
+
|
|
328
339
|
def _unregister_data_point_updated_callback(self, cb: Callable, custom_id: str) -> None:
|
|
329
340
|
"""Unregister data_point updated callback."""
|
|
330
341
|
if cb in self._data_point_updated_callbacks:
|
|
@@ -840,6 +851,10 @@ class BaseParameterDataPoint[
|
|
|
840
851
|
return multiplier
|
|
841
852
|
return DEFAULT_MULTIPLIER
|
|
842
853
|
|
|
854
|
+
def _get_signature(self) -> str:
|
|
855
|
+
"""Return the signature of the data_point."""
|
|
856
|
+
return f"{self._category}/{self._channel.device.model}/{self._parameter}"
|
|
857
|
+
|
|
843
858
|
@abstractmethod
|
|
844
859
|
async def event(self, value: Any, received_at: datetime | None = None) -> None:
|
|
845
860
|
"""Handle event for which this handler has subscribed."""
|
aiohomematic/model/device.py
CHANGED
|
@@ -671,10 +671,11 @@ class Device(PayloadMixin):
|
|
|
671
671
|
"""Provide some useful information."""
|
|
672
672
|
return (
|
|
673
673
|
f"address: {self._address}, "
|
|
674
|
-
f"model: {
|
|
674
|
+
f"model: {self._model}, "
|
|
675
675
|
f"name: {self._name}, "
|
|
676
|
-
f"
|
|
677
|
-
f"
|
|
676
|
+
f"generic dps: {len(self.generic_data_points)}, "
|
|
677
|
+
f"calculated dps: {len(self.calculated_data_points)}, "
|
|
678
|
+
f"custom dps: {len(self.custom_data_points)}, "
|
|
678
679
|
f"events: {len(self.generic_events)}"
|
|
679
680
|
)
|
|
680
681
|
|
|
@@ -1077,8 +1078,9 @@ class Channel(PayloadMixin):
|
|
|
1077
1078
|
return (
|
|
1078
1079
|
f"address: {self._address}, "
|
|
1079
1080
|
f"type: {self._type_name}, "
|
|
1080
|
-
f"
|
|
1081
|
-
f"
|
|
1081
|
+
f"generic dps: {len(self._generic_data_points)}, "
|
|
1082
|
+
f"calculated dps: {len(self._calculated_data_points)}, "
|
|
1083
|
+
f"custom dp: {self._custom_data_point is not None}, "
|
|
1082
1084
|
f"events: {len(self._generic_events)}"
|
|
1083
1085
|
)
|
|
1084
1086
|
|
|
@@ -107,6 +107,10 @@ class GenericHubDataPoint(CallbackDataPoint, PayloadMixin):
|
|
|
107
107
|
"""Return, if the state is uncertain."""
|
|
108
108
|
return self._state_uncertain
|
|
109
109
|
|
|
110
|
+
def _get_signature(self) -> str:
|
|
111
|
+
"""Return the signature of the data_point."""
|
|
112
|
+
return f"{self._category}/{self.name}"
|
|
113
|
+
|
|
110
114
|
|
|
111
115
|
class GenericSysvarDataPoint(GenericHubDataPoint):
|
|
112
116
|
"""Class for a HomeMatic system variable."""
|
aiohomematic/model/update.py
CHANGED
|
@@ -96,6 +96,10 @@ class DpUpdate(CallbackDataPoint, PayloadMixin):
|
|
|
96
96
|
return self._device.available_firmware
|
|
97
97
|
return self._device.firmware
|
|
98
98
|
|
|
99
|
+
def _get_signature(self) -> str:
|
|
100
|
+
"""Return the signature of the data_point."""
|
|
101
|
+
return f"{self._category}/{self._device.model}"
|
|
102
|
+
|
|
99
103
|
def _get_path_data(self) -> DataPointPathData:
|
|
100
104
|
"""Return the path data of the data_point."""
|
|
101
105
|
return DataPointPathData(
|
aiohomematic/support.py
CHANGED
|
@@ -18,6 +18,8 @@ import ssl
|
|
|
18
18
|
import sys
|
|
19
19
|
from typing import Any, Final, cast
|
|
20
20
|
|
|
21
|
+
import orjson
|
|
22
|
+
|
|
21
23
|
from aiohomematic import client as hmcl
|
|
22
24
|
from aiohomematic.const import (
|
|
23
25
|
ADDRESS_SEPARATOR,
|
|
@@ -430,9 +432,20 @@ def debug_enabled() -> bool:
|
|
|
430
432
|
|
|
431
433
|
|
|
432
434
|
def hash_sha256(value: Any) -> str:
|
|
433
|
-
"""
|
|
435
|
+
"""
|
|
436
|
+
Hash a value with sha256.
|
|
437
|
+
|
|
438
|
+
Uses orjson to serialize the value with sorted keys for a fast and stable
|
|
439
|
+
representation. Falls back to the repr-based approach if
|
|
440
|
+
serialization fails (e.g., unsupported types).
|
|
441
|
+
"""
|
|
434
442
|
hasher = hashlib.sha256()
|
|
435
|
-
|
|
443
|
+
try:
|
|
444
|
+
data = orjson.dumps(value, option=orjson.OPT_SORT_KEYS | orjson.OPT_NON_STR_KEYS)
|
|
445
|
+
except Exception:
|
|
446
|
+
# Fallback: convert to a hashable representation and use repr()
|
|
447
|
+
data = repr(_make_value_hashable(value)).encode()
|
|
448
|
+
hasher.update(data)
|
|
436
449
|
return base64.b64encode(hasher.digest()).decode()
|
|
437
450
|
|
|
438
451
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic
|
|
3
|
-
Version: 2025.8.
|
|
3
|
+
Version: 2025.8.9
|
|
4
4
|
Summary: Homematic interface for Home Assistant running on Python 3.
|
|
5
5
|
Home-page: https://github.com/sukramj/aiohomematic
|
|
6
6
|
Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
|