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.

@@ -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."""
@@ -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
- serial: str = json_result[_JsonKey.SERIAL]
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.2"
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.05 if _TEST_SPEEDUP else 10
128
- SCHEDULER_LOOP_SLEEP: Final = 0.05 if _TEST_SPEEDUP else 5
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 CommandRxMode(StrEnum):
534
- """Enum for Homematic rx modes for commands."""
542
+ class SourceOfDeviceCreation(StrEnum):
543
+ """Enum with source of device creation."""
535
544
 
536
- BURST = "BURST"
537
- WAKEUP = "WAKEUP"
545
+ CACHE = "CACHE"
546
+ INIT = "INIT"
547
+ MANUAL = "MANUAL"
548
+ NEW = "NEW"
549
+ REFRESH = "REFRESH"
538
550
 
539
551
 
540
552
  class SysvarType(StrEnum):
@@ -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
- start = (
123
- monotonic() if measure_performance and _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG) else None
124
- )
125
- token = IN_SERVICE_VAR.set(True) if not IN_SERVICE_VAR.get() else None
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=IN_SERVICE_VAR.get(),
138
+ is_sub_service_call=was_in_service,
135
139
  is_homematic=True,
136
- context_obj=(args[0] if args else None),
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=IN_SERVICE_VAR.get(),
148
+ is_sub_service_call=was_in_service,
145
149
  is_homematic=False,
146
- context_obj=(args[0] if args else None),
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
- start = (
161
- monotonic() if measure_performance and _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG) else None
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] # Await the async call
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=IN_SERVICE_VAR.get(),
178
+ is_sub_service_call=was_in_service,
173
179
  is_homematic=True,
174
- context_obj=(args[0] if args else None),
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=IN_SERVICE_VAR.get(),
188
+ is_sub_service_call=was_in_service,
183
189
  is_homematic=False,
184
- context_obj=(args[0] if args else None),
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:
@@ -999,6 +999,7 @@ class CallParameterCollector:
999
999
 
1000
1000
  def add_data_point(
1001
1001
  self,
1002
+ *,
1002
1003
  data_point: BaseParameterDataPoint,
1003
1004
  value: Any,
1004
1005
  collector_order: int,
@@ -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
@@ -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}" # Default name of the cache attribute
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
- if self.fget is None:
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 self.fget(instance)
161
+ return fget(instance)
159
162
 
160
- # If the cached value is not set yet, compute and store it
161
- if not hasattr(instance, self._cache_attr):
162
- value = self.fget(instance)
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
- # Return the cached value
166
- return cast(GETTER, getattr(instance, self._cache_attr))
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 and hasattr(instance, self._cache_attr):
172
- delattr(instance, self._cache_attr)
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 and hasattr(instance, self._cache_attr):
183
- delattr(instance, self._cache_attr)
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
- compare_with = compare_with.lower() if ignore_case else compare_with
375
-
376
- if isinstance(search_elements, str):
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 compare_with:
450
+ if element in compare_with_processed:
396
451
  return True
397
452
  elif do_left_wildcard_search:
398
- if compare_with.endswith(element):
453
+ if compare_with_processed.endswith(element):
399
454
  return True
400
455
  elif do_right_wildcard_search:
401
- if compare_with.startswith(element):
456
+ if compare_with_processed.startswith(element):
402
457
  return True
403
- elif compare_with == element:
458
+ elif compare_with_processed == element:
404
459
  return True
405
- return False
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.2
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=gqyZH8x5dYfRsS77pm28euMwG6GYeFWomzSE0dQxzfc,6134
3
- aiohomematic/const.py,sha256=HZ24tNwS3R9AwEVSX3o6KrUgXBZuNly5NbuAnFeiAkE,25565
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=oHEFSJoJVd-h9RYb6NLTJ60erkpRHPYv7EEipKn1a9Q,10636
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=TN07L9uAos2hB7aqaSIRPziYowWWJXSDFnZqxg0WFhs,16201
9
+ aiohomematic/property_decorators.py,sha256=56lHGATgRtaFkIK_IXcR2tBW9mIVITcCwH5KOw575GA,17162
10
10
  aiohomematic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- aiohomematic/support.py,sha256=w05YlO1IrelF2a1zKkUGWyr5E-nDJ-knh0LQqOHpR34,20514
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=vyQsjBc2uGoNCCrWPMGmNBlUQ9GYvgVuupA9bhqH5hI,21466
15
- aiohomematic/caches/persistent.py,sha256=2bgASru4zbrFEd8rOn2YcReeHPiPJE8hMgMUPAbGh68,20117
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=zmZGMnRhTv_sooFLjwQ0_MRs2DEQ-EvwtGGYcu934kw,86937
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=rf3fFakCX7p7eYp2XLKWZUHWHervrDPysKcWKULAlpM,10602
20
- aiohomematic/client/__init__.py,sha256=XWHg8VTtiahu7jfHwq4yOVXOxUy8nw80JF9uyY7QNJI,70457
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=k5euUJkDzIQXdOX5JJxkua_7AUUTFspK8rTGnVKoCf8,49568
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=hpC-AmymEgx823zgRvPI0HhMq529AsvIDTG-UxISZ4A,41575
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=kr2k3hZQatm2EYtaL-pywbVRXNmrqunb2dPfNrVjxV8,19651
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=KcUwADlqfbnYSoFYLbcSYMf_dOgfc_OdUJxyd7MpcGw,9763
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=bhCMhiIjnrl6GUeZ7RQhg20Tg2GsuUkqY1EOZOp67Fg,6073
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.2.dist-info/licenses/LICENSE,sha256=q-B0xpREuZuvKsmk3_iyVZqvZ-vJcWmzMZpeAd0RqtQ,1083
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=nDG6nmpMHjq8tDUEmE4mK6veGORtIsGqcr6hbGjI2rQ,12759
75
- aiohomematic-2025.10.2.dist-info/METADATA,sha256=2pXDu9_TySGqUPkJvKpM8C9EjpIROMurxQl8n6pRvis,7603
76
- aiohomematic-2025.10.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
77
- aiohomematic-2025.10.2.dist-info/top_level.txt,sha256=5TDRlUWQPThIUwQjOj--aUo4UA-ow4m0sNhnoCBi5n8,34
78
- aiohomematic-2025.10.2.dist-info/RECORD,,
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,,