aiohomematic 2025.10.2__py3-none-any.whl → 2025.10.4__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 +59 -23
- aiohomematic/caches/dynamic.py +27 -14
- aiohomematic/caches/persistent.py +12 -2
- aiohomematic/central/__init__.py +172 -46
- aiohomematic/central/xml_rpc_server.py +6 -1
- aiohomematic/client/__init__.py +23 -0
- aiohomematic/client/json_rpc.py +10 -1
- aiohomematic/const.py +29 -17
- aiohomematic/decorators.py +33 -27
- aiohomematic/model/custom/siren.py +2 -0
- aiohomematic/model/data_point.py +1 -0
- aiohomematic/model/generic/data_point.py +1 -1
- aiohomematic/model/support.py +2 -2
- aiohomematic/property_decorators.py +40 -13
- aiohomematic/support.py +83 -20
- {aiohomematic-2025.10.2.dist-info → aiohomematic-2025.10.4.dist-info}/METADATA +1 -1
- {aiohomematic-2025.10.2.dist-info → aiohomematic-2025.10.4.dist-info}/RECORD +21 -21
- aiohomematic_support/client_local.py +8 -3
- {aiohomematic-2025.10.2.dist-info → aiohomematic-2025.10.4.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.10.2.dist-info → aiohomematic-2025.10.4.dist-info}/licenses/LICENSE +0 -0
- {aiohomematic-2025.10.2.dist-info → aiohomematic-2025.10.4.dist-info}/top_level.txt +0 -0
aiohomematic/client/__init__.py
CHANGED
|
@@ -494,6 +494,29 @@ class Client(ABC, LogContextMixin):
|
|
|
494
494
|
_LOGGER.warning("GET_DEVICE_DESCRIPTIONS failed: %s [%s]", bhexc.name, extract_exc_args(exc=bhexc))
|
|
495
495
|
return None
|
|
496
496
|
|
|
497
|
+
@inspector(re_raise=False)
|
|
498
|
+
async def get_all_device_description(self, *, device_address: str) -> tuple[DeviceDescription, ...] | None:
|
|
499
|
+
"""Get all device descriptions from the backend."""
|
|
500
|
+
all_device_description: list[DeviceDescription] = []
|
|
501
|
+
if main_dd := await self.get_device_description(device_address=device_address):
|
|
502
|
+
all_device_description.append(main_dd)
|
|
503
|
+
else:
|
|
504
|
+
_LOGGER.warning(
|
|
505
|
+
"GET_ALL_DEVICE_DESCRIPTIONS: No device description for %s",
|
|
506
|
+
device_address,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
if main_dd:
|
|
510
|
+
for channel_address in main_dd["CHILDREN"]:
|
|
511
|
+
if channel_dd := await self.get_device_description(device_address=channel_address):
|
|
512
|
+
all_device_description.append(channel_dd)
|
|
513
|
+
else:
|
|
514
|
+
_LOGGER.warning(
|
|
515
|
+
"GET_ALL_DEVICE_DESCRIPTIONS: No channel description for %s",
|
|
516
|
+
channel_address,
|
|
517
|
+
)
|
|
518
|
+
return tuple(all_device_description)
|
|
519
|
+
|
|
497
520
|
@inspector
|
|
498
521
|
async def add_link(self, *, sender_address: str, receiver_address: str, name: str, description: str) -> None:
|
|
499
522
|
"""Return a list of links."""
|
aiohomematic/client/json_rpc.py
CHANGED
|
@@ -1259,7 +1259,16 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
1259
1259
|
response = await self._post_script(script_name=RegaScript.GET_SERIAL)
|
|
1260
1260
|
|
|
1261
1261
|
if json_result := response[_JsonKey.RESULT]:
|
|
1262
|
-
|
|
1262
|
+
# The backend may return a JSON string which needs to be decoded first
|
|
1263
|
+
# or an already-parsed dict. Support both.
|
|
1264
|
+
if isinstance(json_result, str):
|
|
1265
|
+
try:
|
|
1266
|
+
json_result = orjson.loads(json_result)
|
|
1267
|
+
except Exception:
|
|
1268
|
+
# Fall back to plain string handling; return last 10 chars
|
|
1269
|
+
serial_exc = str(json_result)
|
|
1270
|
+
return serial_exc[-10:] if len(serial_exc) > 10 else serial_exc
|
|
1271
|
+
serial: str = str(json_result.get(_JsonKey.SERIAL) if isinstance(json_result, dict) else json_result)
|
|
1263
1272
|
if len(serial) > 10:
|
|
1264
1273
|
serial = serial[-10:]
|
|
1265
1274
|
return serial
|
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.4"
|
|
23
23
|
|
|
24
24
|
# Detect test speedup mode via environment
|
|
25
25
|
_TEST_SPEEDUP: Final = (
|
|
@@ -29,6 +29,7 @@ _TEST_SPEEDUP: Final = (
|
|
|
29
29
|
# default
|
|
30
30
|
DEFAULT_STORAGE_FOLDER: Final = "aiohomematic_storage"
|
|
31
31
|
DEFAULT_CUSTOM_ID: Final = "custom_id"
|
|
32
|
+
DEFAULT_DELAY_NEW_DEVICE_CREATION: Final = False
|
|
32
33
|
DEFAULT_ENABLE_DEVICE_FIRMWARE_CHECK: Final = False
|
|
33
34
|
DEFAULT_ENABLE_PROGRAM_SCAN: Final = True
|
|
34
35
|
DEFAULT_ENABLE_SYSVAR_SCAN: Final = True
|
|
@@ -124,8 +125,8 @@ UN_IGNORE_WILDCARD: Final = "all"
|
|
|
124
125
|
WAIT_FOR_CALLBACK: Final[int | None] = None
|
|
125
126
|
|
|
126
127
|
# Scheduler sleep durations (used by central scheduler loop)
|
|
127
|
-
SCHEDULER_NOT_STARTED_SLEEP: Final = 0.
|
|
128
|
-
SCHEDULER_LOOP_SLEEP: Final = 0.
|
|
128
|
+
SCHEDULER_NOT_STARTED_SLEEP: Final = 0.2 if _TEST_SPEEDUP else 10
|
|
129
|
+
SCHEDULER_LOOP_SLEEP: Final = 0.2 if _TEST_SPEEDUP else 5
|
|
129
130
|
|
|
130
131
|
CALLBACK_WARN_INTERVAL: Final = CONNECTION_CHECKER_INTERVAL * 40
|
|
131
132
|
|
|
@@ -155,6 +156,7 @@ class BackendSystemEvent(StrEnum):
|
|
|
155
156
|
|
|
156
157
|
DELETE_DEVICES = "deleteDevices"
|
|
157
158
|
DEVICES_CREATED = "devicesCreated"
|
|
159
|
+
DEVICES_DELAYED = "devicesDelayed"
|
|
158
160
|
ERROR = "error"
|
|
159
161
|
HUB_REFRESHED = "hubDataPointRefreshed"
|
|
160
162
|
LIST_DEVICES = "listDevices"
|
|
@@ -172,6 +174,16 @@ class CallSource(StrEnum):
|
|
|
172
174
|
MANUAL_OR_SCHEDULED = "manual_or_scheduled"
|
|
173
175
|
|
|
174
176
|
|
|
177
|
+
class CalulatedParameter(StrEnum):
|
|
178
|
+
"""Enum with calculated Homematic parameters."""
|
|
179
|
+
|
|
180
|
+
APPARENT_TEMPERATURE = "APPARENT_TEMPERATURE"
|
|
181
|
+
DEW_POINT = "DEW_POINT"
|
|
182
|
+
FROST_POINT = "FROST_POINT"
|
|
183
|
+
OPERATING_VOLTAGE_LEVEL = "OPERATING_VOLTAGE_LEVEL"
|
|
184
|
+
VAPOR_CONCENTRATION = "VAPOR_CONCENTRATION"
|
|
185
|
+
|
|
186
|
+
|
|
175
187
|
class CentralUnitState(StrEnum):
|
|
176
188
|
"""Enum with central unit states."""
|
|
177
189
|
|
|
@@ -183,6 +195,13 @@ class CentralUnitState(StrEnum):
|
|
|
183
195
|
STOPPING = "stopping"
|
|
184
196
|
|
|
185
197
|
|
|
198
|
+
class CommandRxMode(StrEnum):
|
|
199
|
+
"""Enum for Homematic rx modes for commands."""
|
|
200
|
+
|
|
201
|
+
BURST = "BURST"
|
|
202
|
+
WAKEUP = "WAKEUP"
|
|
203
|
+
|
|
204
|
+
|
|
186
205
|
class DataOperationResult(Enum):
|
|
187
206
|
"""Enum with data operation results."""
|
|
188
207
|
|
|
@@ -321,16 +340,6 @@ class Operations(IntEnum):
|
|
|
321
340
|
EVENT = 4
|
|
322
341
|
|
|
323
342
|
|
|
324
|
-
class CalulatedParameter(StrEnum):
|
|
325
|
-
"""Enum with calculated Homematic parameters."""
|
|
326
|
-
|
|
327
|
-
APPARENT_TEMPERATURE = "APPARENT_TEMPERATURE"
|
|
328
|
-
DEW_POINT = "DEW_POINT"
|
|
329
|
-
FROST_POINT = "FROST_POINT"
|
|
330
|
-
OPERATING_VOLTAGE_LEVEL = "OPERATING_VOLTAGE_LEVEL"
|
|
331
|
-
VAPOR_CONCENTRATION = "VAPOR_CONCENTRATION"
|
|
332
|
-
|
|
333
|
-
|
|
334
343
|
class Parameter(StrEnum):
|
|
335
344
|
"""Enum with Homematic parameters."""
|
|
336
345
|
|
|
@@ -530,11 +539,14 @@ class RxMode(IntEnum):
|
|
|
530
539
|
LAZY_CONFIG = 16
|
|
531
540
|
|
|
532
541
|
|
|
533
|
-
class
|
|
534
|
-
"""Enum
|
|
542
|
+
class SourceOfDeviceCreation(StrEnum):
|
|
543
|
+
"""Enum with source of device creation."""
|
|
535
544
|
|
|
536
|
-
|
|
537
|
-
|
|
545
|
+
CACHE = "CACHE"
|
|
546
|
+
INIT = "INIT"
|
|
547
|
+
MANUAL = "MANUAL"
|
|
548
|
+
NEW = "NEW"
|
|
549
|
+
REFRESH = "REFRESH"
|
|
538
550
|
|
|
539
551
|
|
|
540
552
|
class SysvarType(StrEnum):
|
aiohomematic/decorators.py
CHANGED
|
@@ -117,78 +117,84 @@ def inspector[**P, R]( # noqa: C901
|
|
|
117
117
|
|
|
118
118
|
@wraps(func)
|
|
119
119
|
def wrap_sync_function(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
120
|
-
"""Wrap sync functions."""
|
|
120
|
+
"""Wrap sync functions with minimized per-call overhead."""
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
125
|
-
|
|
122
|
+
# Fast-path: avoid logger check and time call unless explicitly enabled
|
|
123
|
+
start_needed = measure_performance and _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG)
|
|
124
|
+
start = monotonic() if start_needed else None
|
|
125
|
+
|
|
126
|
+
# Avoid repeated ContextVar.get() calls; only set/reset when needed
|
|
127
|
+
was_in_service = IN_SERVICE_VAR.get()
|
|
128
|
+
token = IN_SERVICE_VAR.set(True) if not was_in_service else None
|
|
129
|
+
context_obj = args[0] if args else None
|
|
126
130
|
try:
|
|
127
131
|
return_value: R = func(*args, **kwargs)
|
|
128
132
|
except BaseHomematicException as bhexc:
|
|
129
|
-
if token:
|
|
133
|
+
if token is not None:
|
|
130
134
|
IN_SERVICE_VAR.reset(token)
|
|
131
135
|
return handle_exception(
|
|
132
136
|
exc=bhexc,
|
|
133
137
|
func=func,
|
|
134
|
-
is_sub_service_call=
|
|
138
|
+
is_sub_service_call=was_in_service,
|
|
135
139
|
is_homematic=True,
|
|
136
|
-
context_obj=
|
|
140
|
+
context_obj=context_obj,
|
|
137
141
|
)
|
|
138
142
|
except Exception as exc:
|
|
139
|
-
if token:
|
|
143
|
+
if token is not None:
|
|
140
144
|
IN_SERVICE_VAR.reset(token)
|
|
141
145
|
return handle_exception(
|
|
142
146
|
exc=exc,
|
|
143
147
|
func=func,
|
|
144
|
-
is_sub_service_call=
|
|
148
|
+
is_sub_service_call=was_in_service,
|
|
145
149
|
is_homematic=False,
|
|
146
|
-
context_obj=
|
|
150
|
+
context_obj=context_obj,
|
|
147
151
|
)
|
|
148
152
|
else:
|
|
149
|
-
if token:
|
|
153
|
+
if token is not None:
|
|
150
154
|
IN_SERVICE_VAR.reset(token)
|
|
151
155
|
return return_value
|
|
152
156
|
finally:
|
|
153
|
-
if start:
|
|
157
|
+
if start is not None:
|
|
154
158
|
_log_performance_message(func, start, *args, **kwargs)
|
|
155
159
|
|
|
156
160
|
@wraps(func)
|
|
157
161
|
async def wrap_async_function(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
158
|
-
"""Wrap async functions."""
|
|
162
|
+
"""Wrap async functions with minimized per-call overhead."""
|
|
163
|
+
|
|
164
|
+
start_needed = measure_performance and _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG)
|
|
165
|
+
start = monotonic() if start_needed else None
|
|
159
166
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
token = IN_SERVICE_VAR.set(True) if not IN_SERVICE_VAR.get() else None
|
|
167
|
+
was_in_service = IN_SERVICE_VAR.get()
|
|
168
|
+
token = IN_SERVICE_VAR.set(True) if not was_in_service else None
|
|
169
|
+
context_obj = args[0] if args else None
|
|
164
170
|
try:
|
|
165
|
-
return_value = await func(*args, **kwargs) # type: ignore[misc]
|
|
171
|
+
return_value = await func(*args, **kwargs) # type: ignore[misc]
|
|
166
172
|
except BaseHomematicException as bhexc:
|
|
167
|
-
if token:
|
|
173
|
+
if token is not None:
|
|
168
174
|
IN_SERVICE_VAR.reset(token)
|
|
169
175
|
return handle_exception(
|
|
170
176
|
exc=bhexc,
|
|
171
177
|
func=func,
|
|
172
|
-
is_sub_service_call=
|
|
178
|
+
is_sub_service_call=was_in_service,
|
|
173
179
|
is_homematic=True,
|
|
174
|
-
context_obj=
|
|
180
|
+
context_obj=context_obj,
|
|
175
181
|
)
|
|
176
182
|
except Exception as exc:
|
|
177
|
-
if token:
|
|
183
|
+
if token is not None:
|
|
178
184
|
IN_SERVICE_VAR.reset(token)
|
|
179
185
|
return handle_exception(
|
|
180
186
|
exc=exc,
|
|
181
187
|
func=func,
|
|
182
|
-
is_sub_service_call=
|
|
188
|
+
is_sub_service_call=was_in_service,
|
|
183
189
|
is_homematic=False,
|
|
184
|
-
context_obj=
|
|
190
|
+
context_obj=context_obj,
|
|
185
191
|
)
|
|
186
192
|
else:
|
|
187
|
-
if token:
|
|
193
|
+
if token is not None:
|
|
188
194
|
IN_SERVICE_VAR.reset(token)
|
|
189
195
|
return cast(R, return_value)
|
|
190
196
|
finally:
|
|
191
|
-
if start:
|
|
197
|
+
if start is not None:
|
|
192
198
|
_log_performance_message(func, start, *args, **kwargs)
|
|
193
199
|
|
|
194
200
|
# Check if the function is a coroutine or not and select the appropriate wrapper
|
|
@@ -79,6 +79,7 @@ class BaseCustomDpSiren(CustomDataPoint):
|
|
|
79
79
|
@bind_collector()
|
|
80
80
|
async def turn_on(
|
|
81
81
|
self,
|
|
82
|
+
*,
|
|
82
83
|
collector: CallParameterCollector | None = None,
|
|
83
84
|
**kwargs: Unpack[SirenOnArgs],
|
|
84
85
|
) -> None:
|
|
@@ -143,6 +144,7 @@ class CustomDpIpSiren(BaseCustomDpSiren):
|
|
|
143
144
|
@bind_collector()
|
|
144
145
|
async def turn_on(
|
|
145
146
|
self,
|
|
147
|
+
*,
|
|
146
148
|
collector: CallParameterCollector | None = None,
|
|
147
149
|
**kwargs: Unpack[SirenOnArgs],
|
|
148
150
|
) -> None:
|
aiohomematic/model/data_point.py
CHANGED
|
@@ -111,7 +111,7 @@ class GenericDataPoint[ParameterT: GenericParameterType, InputParameterT: Generi
|
|
|
111
111
|
converted_value = self._convert_value(value=prepared_value)
|
|
112
112
|
# if collector is set, then add value to collector
|
|
113
113
|
if collector:
|
|
114
|
-
collector.add_data_point(self, value=converted_value, collector_order=collector_order)
|
|
114
|
+
collector.add_data_point(data_point=self, value=converted_value, collector_order=collector_order)
|
|
115
115
|
return set()
|
|
116
116
|
|
|
117
117
|
# if collector is not set, then send value directly
|
aiohomematic/model/support.py
CHANGED
|
@@ -87,7 +87,7 @@ class ChannelNameData:
|
|
|
87
87
|
return ChannelNameData(device_name="", channel_name="")
|
|
88
88
|
|
|
89
89
|
@staticmethod
|
|
90
|
-
def _get_channel_name(device_name: str, channel_name: str) -> str:
|
|
90
|
+
def _get_channel_name(*, device_name: str, channel_name: str) -> str:
|
|
91
91
|
"""Return the channel_name of the data_point only name."""
|
|
92
92
|
if device_name and channel_name and channel_name.startswith(device_name):
|
|
93
93
|
c_name = channel_name.replace(device_name, "").strip()
|
|
@@ -121,7 +121,7 @@ class DataPointNameData(ChannelNameData):
|
|
|
121
121
|
return DataPointNameData(device_name="", channel_name="")
|
|
122
122
|
|
|
123
123
|
@staticmethod
|
|
124
|
-
def _get_channel_parameter_name(channel_name: str, parameter_name: str | None) -> str:
|
|
124
|
+
def _get_channel_parameter_name(*, channel_name: str, parameter_name: str | None) -> str:
|
|
125
125
|
"""Return the channel parameter name of the data_point."""
|
|
126
126
|
if channel_name and parameter_name:
|
|
127
127
|
return f"{channel_name} {parameter_name}".strip()
|
|
@@ -65,6 +65,8 @@ class _GenericProperty[GETTER, SETTER](property):
|
|
|
65
65
|
|
|
66
66
|
"""
|
|
67
67
|
|
|
68
|
+
__kwonly_check__ = False
|
|
69
|
+
|
|
68
70
|
fget: Callable[[Any], GETTER] | None
|
|
69
71
|
fset: Callable[[Any, SETTER], None] | None
|
|
70
72
|
fdel: Callable[[Any], None] | None
|
|
@@ -103,7 +105,7 @@ class _GenericProperty[GETTER, SETTER](property):
|
|
|
103
105
|
func_name = fdel.__name__
|
|
104
106
|
else:
|
|
105
107
|
func_name = "prop"
|
|
106
|
-
self._cache_attr = f"_cached_{func_name}"
|
|
108
|
+
self._cache_attr = f"_cached_{func_name}"
|
|
107
109
|
|
|
108
110
|
def getter(self, fget: Callable[[Any], GETTER], /) -> _GenericProperty:
|
|
109
111
|
"""Return generic getter."""
|
|
@@ -151,25 +153,45 @@ class _GenericProperty[GETTER, SETTER](property):
|
|
|
151
153
|
if instance is None:
|
|
152
154
|
# Accessed from class, return the descriptor itself
|
|
153
155
|
return cast(GETTER, self)
|
|
154
|
-
|
|
156
|
+
|
|
157
|
+
if (fget := self.fget) is None:
|
|
155
158
|
raise AttributeError("unreadable attribute") # pragma: no cover
|
|
156
159
|
|
|
157
160
|
if not self._cached:
|
|
158
|
-
return
|
|
161
|
+
return fget(instance)
|
|
159
162
|
|
|
160
|
-
#
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
setattr(instance, self._cache_attr, value)
|
|
163
|
+
# Use direct __dict__ access when available for better performance
|
|
164
|
+
# Store cache_attr in local variable to avoid repeated attribute lookup
|
|
165
|
+
cache_attr = self._cache_attr
|
|
164
166
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
try:
|
|
168
|
+
inst_dict = instance.__dict__
|
|
169
|
+
# Use 'in' check first to distinguish between missing and None
|
|
170
|
+
if cache_attr in inst_dict:
|
|
171
|
+
return cast(GETTER, inst_dict[cache_attr])
|
|
172
|
+
|
|
173
|
+
# Not cached yet, compute and store
|
|
174
|
+
value = fget(instance)
|
|
175
|
+
inst_dict[cache_attr] = value
|
|
176
|
+
except AttributeError:
|
|
177
|
+
# Object uses __slots__, fall back to getattr/setattr
|
|
178
|
+
try:
|
|
179
|
+
return cast(GETTER, getattr(instance, cache_attr))
|
|
180
|
+
except AttributeError:
|
|
181
|
+
value = fget(instance)
|
|
182
|
+
setattr(instance, cache_attr, value)
|
|
183
|
+
return value
|
|
167
184
|
|
|
168
185
|
def __set__(self, instance: Any, value: Any, /) -> None:
|
|
169
186
|
"""Set the attribute value and invalidate cache if enabled."""
|
|
170
187
|
# Delete the cached value so it can be recomputed on next access.
|
|
171
|
-
if self._cached
|
|
172
|
-
|
|
188
|
+
if self._cached:
|
|
189
|
+
try:
|
|
190
|
+
instance.__dict__.pop(self._cache_attr, None)
|
|
191
|
+
except AttributeError:
|
|
192
|
+
# Object uses __slots__, fall back to delattr
|
|
193
|
+
if hasattr(instance, self._cache_attr):
|
|
194
|
+
delattr(instance, self._cache_attr)
|
|
173
195
|
|
|
174
196
|
if self.fset is None:
|
|
175
197
|
raise AttributeError("can't set attribute") # pragma: no cover
|
|
@@ -179,8 +201,13 @@ class _GenericProperty[GETTER, SETTER](property):
|
|
|
179
201
|
"""Delete the attribute and invalidate cache if enabled."""
|
|
180
202
|
|
|
181
203
|
# Delete the cached value so it can be recomputed on next access.
|
|
182
|
-
if self._cached
|
|
183
|
-
|
|
204
|
+
if self._cached:
|
|
205
|
+
try:
|
|
206
|
+
instance.__dict__.pop(self._cache_attr, None)
|
|
207
|
+
except AttributeError:
|
|
208
|
+
# Object uses __slots__, fall back to delattr
|
|
209
|
+
if hasattr(instance, self._cache_attr):
|
|
210
|
+
delattr(instance, self._cache_attr)
|
|
184
211
|
|
|
185
212
|
if self.fdel is None:
|
|
186
213
|
raise AttributeError("can't delete attribute") # pragma: no cover
|
aiohomematic/support.py
CHANGED
|
@@ -44,6 +44,7 @@ from aiohomematic.const import (
|
|
|
44
44
|
PRIMARY_CLIENT_CANDIDATE_INTERFACES,
|
|
45
45
|
TIMEOUT,
|
|
46
46
|
CommandRxMode,
|
|
47
|
+
DeviceDescription,
|
|
47
48
|
ParamsetKey,
|
|
48
49
|
RxMode,
|
|
49
50
|
SysvarType,
|
|
@@ -165,6 +166,19 @@ def check_or_create_directory(*, directory: str) -> bool:
|
|
|
165
166
|
return True
|
|
166
167
|
|
|
167
168
|
|
|
169
|
+
def extract_device_addresses_from_device_descriptions(
|
|
170
|
+
*, device_descriptions: tuple[DeviceDescription, ...]
|
|
171
|
+
) -> tuple[str, ...]:
|
|
172
|
+
"""Extract addresses from device descriptions."""
|
|
173
|
+
return tuple(
|
|
174
|
+
{
|
|
175
|
+
parent_address
|
|
176
|
+
for dev_desc in device_descriptions
|
|
177
|
+
if (parent_address := dev_desc["PARENT"]) is not None and (is_device_address(address=parent_address))
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
168
182
|
def parse_sys_var(*, data_type: SysvarType | None, raw_value: Any) -> Any:
|
|
169
183
|
"""Parse system variables to fix type."""
|
|
170
184
|
if not data_type:
|
|
@@ -352,6 +366,45 @@ def is_port(*, port: int) -> bool:
|
|
|
352
366
|
return 0 <= port <= 65535
|
|
353
367
|
|
|
354
368
|
|
|
369
|
+
@lru_cache(maxsize=2048)
|
|
370
|
+
def _element_matches_key_cached(
|
|
371
|
+
*,
|
|
372
|
+
search_elements: tuple[str, ...] | str,
|
|
373
|
+
compare_with: str,
|
|
374
|
+
ignore_case: bool,
|
|
375
|
+
do_left_wildcard_search: bool,
|
|
376
|
+
do_right_wildcard_search: bool,
|
|
377
|
+
) -> bool:
|
|
378
|
+
"""Cache element matching for hashable inputs."""
|
|
379
|
+
compare_with_processed = compare_with.lower() if ignore_case else compare_with
|
|
380
|
+
|
|
381
|
+
if isinstance(search_elements, str):
|
|
382
|
+
element = search_elements.lower() if ignore_case else search_elements
|
|
383
|
+
if do_left_wildcard_search is True and do_right_wildcard_search is True:
|
|
384
|
+
return element in compare_with_processed
|
|
385
|
+
if do_left_wildcard_search:
|
|
386
|
+
return compare_with_processed.endswith(element)
|
|
387
|
+
if do_right_wildcard_search:
|
|
388
|
+
return compare_with_processed.startswith(element)
|
|
389
|
+
return compare_with_processed == element
|
|
390
|
+
|
|
391
|
+
# search_elements is a tuple
|
|
392
|
+
for item in search_elements:
|
|
393
|
+
element = item.lower() if ignore_case else item
|
|
394
|
+
if do_left_wildcard_search is True and do_right_wildcard_search is True:
|
|
395
|
+
if element in compare_with_processed:
|
|
396
|
+
return True
|
|
397
|
+
elif do_left_wildcard_search:
|
|
398
|
+
if compare_with_processed.endswith(element):
|
|
399
|
+
return True
|
|
400
|
+
elif do_right_wildcard_search:
|
|
401
|
+
if compare_with_processed.startswith(element):
|
|
402
|
+
return True
|
|
403
|
+
elif compare_with_processed == element:
|
|
404
|
+
return True
|
|
405
|
+
return False
|
|
406
|
+
|
|
407
|
+
|
|
355
408
|
def element_matches_key(
|
|
356
409
|
*,
|
|
357
410
|
search_elements: str | Collection[str],
|
|
@@ -371,38 +424,48 @@ def element_matches_key(
|
|
|
371
424
|
if compare_with is None or not search_elements:
|
|
372
425
|
return False
|
|
373
426
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
element = search_elements.lower() if ignore_case else search_elements
|
|
378
|
-
if do_left_wildcard_search is True and do_right_wildcard_search is True:
|
|
379
|
-
return element in compare_with
|
|
380
|
-
if do_left_wildcard_search:
|
|
381
|
-
return compare_with.endswith(element)
|
|
382
|
-
if do_right_wildcard_search:
|
|
383
|
-
return compare_with.startswith(element)
|
|
384
|
-
return compare_with == element
|
|
385
|
-
if isinstance(search_elements, Collection):
|
|
386
|
-
if isinstance(search_elements, dict) and (
|
|
387
|
-
match_key := _get_search_key(search_elements=search_elements, search_key=search_key) if search_key else None
|
|
388
|
-
):
|
|
427
|
+
# Handle dict case with search_key
|
|
428
|
+
if isinstance(search_elements, dict) and search_key:
|
|
429
|
+
if match_key := _get_search_key(search_elements=search_elements, search_key=search_key):
|
|
389
430
|
if (elements := search_elements.get(match_key)) is None:
|
|
390
431
|
return False
|
|
391
432
|
search_elements = elements
|
|
433
|
+
else:
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
search_elements_hashable: str | Collection[str]
|
|
437
|
+
# Convert to hashable types for caching
|
|
438
|
+
if isinstance(search_elements, str):
|
|
439
|
+
search_elements_hashable = search_elements
|
|
440
|
+
elif isinstance(search_elements, (list, set)):
|
|
441
|
+
search_elements_hashable = tuple(search_elements)
|
|
442
|
+
elif isinstance(search_elements, tuple):
|
|
443
|
+
search_elements_hashable = search_elements
|
|
444
|
+
else:
|
|
445
|
+
# Fall back to non-cached version for other collection types
|
|
446
|
+
compare_with_processed = compare_with.lower() if ignore_case else compare_with
|
|
392
447
|
for item in search_elements:
|
|
393
448
|
element = item.lower() if ignore_case else item
|
|
394
449
|
if do_left_wildcard_search is True and do_right_wildcard_search is True:
|
|
395
|
-
if element in
|
|
450
|
+
if element in compare_with_processed:
|
|
396
451
|
return True
|
|
397
452
|
elif do_left_wildcard_search:
|
|
398
|
-
if
|
|
453
|
+
if compare_with_processed.endswith(element):
|
|
399
454
|
return True
|
|
400
455
|
elif do_right_wildcard_search:
|
|
401
|
-
if
|
|
456
|
+
if compare_with_processed.startswith(element):
|
|
402
457
|
return True
|
|
403
|
-
elif
|
|
458
|
+
elif compare_with_processed == element:
|
|
404
459
|
return True
|
|
405
|
-
|
|
460
|
+
return False
|
|
461
|
+
|
|
462
|
+
return _element_matches_key_cached(
|
|
463
|
+
search_elements=search_elements_hashable,
|
|
464
|
+
compare_with=compare_with,
|
|
465
|
+
ignore_case=ignore_case,
|
|
466
|
+
do_left_wildcard_search=do_left_wildcard_search,
|
|
467
|
+
do_right_wildcard_search=do_right_wildcard_search,
|
|
468
|
+
)
|
|
406
469
|
|
|
407
470
|
|
|
408
471
|
def _get_search_key(*, search_elements: Collection[str], search_key: str) -> str | None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic
|
|
3
|
-
Version: 2025.10.
|
|
3
|
+
Version: 2025.10.4
|
|
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>
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
aiohomematic/__init__.py,sha256=ngULK_anZQwwUUCVcberBdVjguYfboiuG9VoueKy9fA,2283
|
|
2
|
-
aiohomematic/async_support.py,sha256=
|
|
3
|
-
aiohomematic/const.py,sha256=
|
|
2
|
+
aiohomematic/async_support.py,sha256=BeNKaDrFsRA5-_uAFzmyyKPqlImfSs58C22Nqd5dZAg,7887
|
|
3
|
+
aiohomematic/const.py,sha256=VDLTURjQcPRA2F_A10jED7tuGuE9ZZZtCAbphjLNhNY,25840
|
|
4
4
|
aiohomematic/context.py,sha256=M7gkA7KFT0dp35gzGz2dzKVXu1PP0sAnepgLlmjyRS4,451
|
|
5
5
|
aiohomematic/converter.py,sha256=gaNHe-WEiBStZMuuRz9iGn3Mo_CGz1bjgLtlYBJJAko,3624
|
|
6
|
-
aiohomematic/decorators.py,sha256=
|
|
6
|
+
aiohomematic/decorators.py,sha256=M4n_VSyqmsUgQQQv_-3JWQxYPbS6KEkhCS8OzAfaVKo,11060
|
|
7
7
|
aiohomematic/exceptions.py,sha256=8Uu3rADawhYlAz6y4J52aJ-wKok8Z7YbUYUwWeGMKhs,5028
|
|
8
8
|
aiohomematic/hmcli.py,sha256=qNstNDX6q8t3mJFCGlXlmRVobGabntrPtFi3kchf1Eg,4933
|
|
9
|
-
aiohomematic/property_decorators.py,sha256=
|
|
9
|
+
aiohomematic/property_decorators.py,sha256=56lHGATgRtaFkIK_IXcR2tBW9mIVITcCwH5KOw575GA,17162
|
|
10
10
|
aiohomematic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
aiohomematic/support.py,sha256=
|
|
11
|
+
aiohomematic/support.py,sha256=8sZtyPKh9sSv0dA3dAlTCCsq0UUl6g49-oqka2Coa1s,22810
|
|
12
12
|
aiohomematic/validator.py,sha256=HUikmo-SFksehFBAdZmBv4ajy0XkjgvXvcCfbexnzZo,3563
|
|
13
13
|
aiohomematic/caches/__init__.py,sha256=_gI30tbsWgPRaHvP6cRxOQr6n9bYZzU-jp1WbHhWg-A,470
|
|
14
|
-
aiohomematic/caches/dynamic.py,sha256=
|
|
15
|
-
aiohomematic/caches/persistent.py,sha256=
|
|
14
|
+
aiohomematic/caches/dynamic.py,sha256=0hOu-WoYUc9_3fofMeg_OjlYS-quD4uTyDI6zd5W4Do,22553
|
|
15
|
+
aiohomematic/caches/persistent.py,sha256=xUMjvu5Vthz9W0LLllSbcqTADZvVV025b4VnPzrPnis,20604
|
|
16
16
|
aiohomematic/caches/visibility.py,sha256=SThfEO3LKbIIERD4Novyj4ZZUkcYrBuvYdNQfPO29JQ,31676
|
|
17
|
-
aiohomematic/central/__init__.py,sha256=
|
|
17
|
+
aiohomematic/central/__init__.py,sha256=i2jX_6nWAP59UmssCfbB0dN3DcHIlncpxW8enDinsDA,92211
|
|
18
18
|
aiohomematic/central/decorators.py,sha256=v0gCa5QEQPNEvGy0O2YIOhoJy7OKiJZJWtK64OQm7y4,6918
|
|
19
|
-
aiohomematic/central/xml_rpc_server.py,sha256=
|
|
20
|
-
aiohomematic/client/__init__.py,sha256=
|
|
19
|
+
aiohomematic/central/xml_rpc_server.py,sha256=1Vr8BXakLSo-RC967IlRI9LPUCl4iXp2y-AXSZuDyPc,10777
|
|
20
|
+
aiohomematic/client/__init__.py,sha256=KcqPEsqYF6L5RoW3LoKd83GqyM2F8W6LkA_cBG_sLiw,71519
|
|
21
21
|
aiohomematic/client/_rpc_errors.py,sha256=-NPtGvkQPJ4V2clDxv1tKy09M9JZm61pUCeki9DDh6s,2984
|
|
22
|
-
aiohomematic/client/json_rpc.py,sha256=
|
|
22
|
+
aiohomematic/client/json_rpc.py,sha256=DSTmKB7YdxVmOH5b7HKoN_H-fI837n0CpyPdJyurans,50174
|
|
23
23
|
aiohomematic/client/xml_rpc.py,sha256=9PEOWoTG0EMUAMyqiInF4iA_AduHv_hhjJW3YhYjYqU,9614
|
|
24
24
|
aiohomematic/model/__init__.py,sha256=KO7gas_eEzm67tODKqWTs0617CSGeKKjOWOlDbhRo_Q,5458
|
|
25
|
-
aiohomematic/model/data_point.py,sha256=
|
|
25
|
+
aiohomematic/model/data_point.py,sha256=Ml8AOQ1RcRezTYWiGBlIXwcTLolQMX5Cyb-O7GtNDm4,41586
|
|
26
26
|
aiohomematic/model/device.py,sha256=15z5G2X3jSJaj-yz7jX_tnirzipRIGBJPymObY3Dmjk,52942
|
|
27
27
|
aiohomematic/model/event.py,sha256=82H8M_QNMCCC29mP3R16alJyKWS3Hb3aqY_aFMSqCvo,6874
|
|
28
|
-
aiohomematic/model/support.py,sha256=
|
|
28
|
+
aiohomematic/model/support.py,sha256=l5E9Oon20nkGWOSEmbYtqQbpbh6-H4rIk8xtEtk5Fcg,19657
|
|
29
29
|
aiohomematic/model/update.py,sha256=5F39xNz9B2GKJ8TvJHPMC-Wu97HfkiiMawjnHEYMnoA,5156
|
|
30
30
|
aiohomematic/model/calculated/__init__.py,sha256=UGLePgKDH8JpLqjhPBgvBzjggI34omcaCPsc6tcM8Xs,2811
|
|
31
31
|
aiohomematic/model/calculated/climate.py,sha256=GXBsC5tnrC_BvnFBkJ9KUqE7uVcGD1KTU_6-OleF5H4,8545
|
|
@@ -40,7 +40,7 @@ aiohomematic/model/custom/data_point.py,sha256=l2pTz7Fu5jGCstXHK1cWCFfBWIJeKmtt3
|
|
|
40
40
|
aiohomematic/model/custom/definition.py,sha256=9kSdqVOHQs65Q2Op5QknNQv5lLmZkZlGCUUCRGicOaw,35662
|
|
41
41
|
aiohomematic/model/custom/light.py,sha256=2UxQOoupwTpQ-5iwY51gL_B815sgDXNW-HG-QhAFb9E,44448
|
|
42
42
|
aiohomematic/model/custom/lock.py,sha256=ndzZ0hp7FBohw7T_qR0jPobwlcwxus9M1DuDu_7vfPw,11996
|
|
43
|
-
aiohomematic/model/custom/siren.py,sha256=
|
|
43
|
+
aiohomematic/model/custom/siren.py,sha256=DT8RoOCl7FqstgRSBK-RWRcY4T29LuEdnlhaWCB6ATk,9785
|
|
44
44
|
aiohomematic/model/custom/support.py,sha256=UvencsvCwgpm4iqRNRt5KRs560tyw1NhYP5ZaqmCT2k,1453
|
|
45
45
|
aiohomematic/model/custom/switch.py,sha256=VKknWPJOtSwIzV-Ag_8Zg1evtkyjKh768Be_quU_R54,6885
|
|
46
46
|
aiohomematic/model/custom/valve.py,sha256=u9RYzeJ8FNmpFO6amlLElXTQdAeqac5yo7NbZYS6Z9U,4242
|
|
@@ -48,7 +48,7 @@ aiohomematic/model/generic/__init__.py,sha256=-ho8m9gFlORBGNPn2i8c9i5-GVLLFvTlf5
|
|
|
48
48
|
aiohomematic/model/generic/action.py,sha256=niJPvTs43b9GiKomdBaBKwjOwtmNxR_YRhj5Fpje9NU,997
|
|
49
49
|
aiohomematic/model/generic/binary_sensor.py,sha256=U5GvfRYbhwe0jRmaedD4LVZ_24SyyPbVr74HEfZXoxE,887
|
|
50
50
|
aiohomematic/model/generic/button.py,sha256=6jZ49woI9gYJEx__PjguDNbc5vdE1P-YcLMZZFkYRCg,740
|
|
51
|
-
aiohomematic/model/generic/data_point.py,sha256=
|
|
51
|
+
aiohomematic/model/generic/data_point.py,sha256=2NvdU802JUo4NZh0v6oMI-pVtlNluSFse7ISMGqo70g,6084
|
|
52
52
|
aiohomematic/model/generic/number.py,sha256=nJgOkMZwNfPtzBrX2o5RAjBt-o8KrKuqtDa9LBj0Jw0,2678
|
|
53
53
|
aiohomematic/model/generic/select.py,sha256=vWfLUdQBjZLG-q-WZMxHk9Klawg_iNOEeSoVHrvG35I,1538
|
|
54
54
|
aiohomematic/model/generic/sensor.py,sha256=wCnQ8IoC8uPTN29R250pfJa4x6y9sh4c3vxQ4Km8Clg,2262
|
|
@@ -69,10 +69,10 @@ aiohomematic/rega_scripts/get_serial.fn,sha256=t1oeo-sB_EuVeiY24PLcxFSkdQVgEWGXz
|
|
|
69
69
|
aiohomematic/rega_scripts/get_system_variable_descriptions.fn,sha256=UKXvC0_5lSApdQ2atJc0E5Stj5Zt3lqh0EcliokYu2c,849
|
|
70
70
|
aiohomematic/rega_scripts/set_program_state.fn,sha256=0bnv7lUj8FMjDZBz325tDVP61m04cHjVj4kIOnUUgpY,279
|
|
71
71
|
aiohomematic/rega_scripts/set_system_variable.fn,sha256=sTmr7vkPTPnPkor5cnLKlDvfsYRbGO1iq2z_2pMXq5E,383
|
|
72
|
-
aiohomematic-2025.10.
|
|
72
|
+
aiohomematic-2025.10.4.dist-info/licenses/LICENSE,sha256=q-B0xpREuZuvKsmk3_iyVZqvZ-vJcWmzMZpeAd0RqtQ,1083
|
|
73
73
|
aiohomematic_support/__init__.py,sha256=_0YtF4lTdC_k6-zrM2IefI0u0LMr_WA61gXAyeGLgbY,66
|
|
74
|
-
aiohomematic_support/client_local.py,sha256=
|
|
75
|
-
aiohomematic-2025.10.
|
|
76
|
-
aiohomematic-2025.10.
|
|
77
|
-
aiohomematic-2025.10.
|
|
78
|
-
aiohomematic-2025.10.
|
|
74
|
+
aiohomematic_support/client_local.py,sha256=nFeYkoX_EXXIwbrpL_5peYQG-934D0ASN6kflYp0_4I,12819
|
|
75
|
+
aiohomematic-2025.10.4.dist-info/METADATA,sha256=4PozhxhfpJbt0vBqnm_ZWxwuU46sJcp91I7SBrYG-Kg,7603
|
|
76
|
+
aiohomematic-2025.10.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
77
|
+
aiohomematic-2025.10.4.dist-info/top_level.txt,sha256=5TDRlUWQPThIUwQjOj--aUo4UA-ow4m0sNhnoCBi5n8,34
|
|
78
|
+
aiohomematic-2025.10.4.dist-info/RECORD,,
|