aiohomematic-test-support 2025.12.23__py3-none-any.whl → 2026.1.23__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.
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
1
3
  """
2
4
  Mock implementations for RPC clients with session playback.
3
5
 
@@ -42,6 +44,7 @@ import contextlib
42
44
  import json
43
45
  import logging
44
46
  import os
47
+ import sys
45
48
  from typing import Any, cast
46
49
  from unittest.mock import MagicMock, Mock
47
50
  import zipfile
@@ -49,12 +52,14 @@ import zipfile
49
52
  from aiohttp import ClientSession
50
53
  import orjson
51
54
 
55
+ from aiohomematic.async_support import Looper
52
56
  from aiohomematic.central import CentralUnit
53
- from aiohomematic.client import BaseRpcProxy
57
+ from aiohomematic.client import BaseRpcProxy, CircuitBreaker
54
58
  from aiohomematic.client.json_rpc import _JsonKey, _JsonRpcMethod
55
59
  from aiohomematic.client.rpc_proxy import _RpcMethod
56
60
  from aiohomematic.const import UTF_8, DataOperationResult, Parameter, ParamsetKey, RPCType
57
- from aiohomematic.store.persistent import _cleanup_params_for_session, _freeze_params, _unfreeze_params
61
+ from aiohomematic.property_decorators import DelegatedProperty
62
+ from aiohomematic.store import cleanup_params_for_session, freeze_params, unfreeze_params
58
63
  from aiohomematic_test_support import const
59
64
 
60
65
  _LOGGER = logging.getLogger(__name__)
@@ -65,6 +70,7 @@ _LOGGER = logging.getLogger(__name__)
65
70
  def _get_not_mockable_method_names(*, instance: Any, exclude_methods: set[str]) -> set[str]:
66
71
  """Return all relevant method names for mocking."""
67
72
  methods: set[str] = set(_get_properties(data_object=instance, decorator=property))
73
+ methods |= _get_properties(data_object=instance, decorator=DelegatedProperty)
68
74
 
69
75
  for method in dir(instance):
70
76
  if method in exclude_methods:
@@ -185,8 +191,18 @@ def get_client_session( # noqa: C901
185
191
  }
186
192
  )
187
193
 
194
+ if "fetch_all_device_data" in params[_JsonKey.SCRIPT]:
195
+ # Return empty device data dict for InterfaceClient
196
+ return _MockResponse(
197
+ json_data={
198
+ _JsonKey.ID: 0,
199
+ _JsonKey.RESULT: {},
200
+ _JsonKey.ERROR: None,
201
+ }
202
+ )
203
+
188
204
  if method == _JsonRpcMethod.INTERFACE_SET_VALUE:
189
- await self._central.data_point_event(
205
+ await self._central.event_coordinator.data_point_event(
190
206
  interface_id=params[_JsonKey.INTERFACE],
191
207
  channel_address=params[_JsonKey.ADDRESS],
192
208
  parameter=params[_JsonKey.VALUE_KEY],
@@ -199,7 +215,7 @@ def get_client_session( # noqa: C901
199
215
  channel_address = params[_JsonKey.ADDRESS]
200
216
  values = params[_JsonKey.SET]
201
217
  for param, value in values.items():
202
- await self._central.data_point_event(
218
+ await self._central.event_coordinator.data_point_event(
203
219
  interface_id=interface_id,
204
220
  channel_address=channel_address,
205
221
  parameter=param,
@@ -269,11 +285,18 @@ def get_xml_rpc_proxy( # noqa: C901
269
285
  self._player = player
270
286
  self._supported_methods: tuple[str, ...] = ()
271
287
  self._central: CentralUnit | None = None
288
+ # Real CircuitBreaker to provide actual metrics for tests
289
+ self._circuit_breaker = CircuitBreaker(interface_id="mock-interface", task_scheduler=Looper())
272
290
 
273
291
  def __getattr__(self, name: str) -> Any:
274
292
  # Start of method chain
275
293
  return _Method(name, self._invoke)
276
294
 
295
+ @property
296
+ def circuit_breaker(self) -> CircuitBreaker:
297
+ """Return the circuit breaker for metrics access."""
298
+ return self._circuit_breaker
299
+
277
300
  @property
278
301
  def supported_methods(self) -> tuple[str, ...]:
279
302
  """Return the supported methods."""
@@ -321,11 +344,11 @@ def get_xml_rpc_proxy( # noqa: C901
321
344
 
322
345
  for dd in devices:
323
346
  if ignore_devices_on_create is not None and (
324
- dd["ADDRESS"] in ignore_devices_on_create or dd["PARENT"] in ignore_devices_on_create
347
+ dd["ADDRESS"] in ignore_devices_on_create or dd.get("PARENT") in ignore_devices_on_create
325
348
  ):
326
349
  continue
327
350
  if address_device_translation is not None:
328
- if dd["ADDRESS"] in address_device_translation or dd["PARENT"] in address_device_translation:
351
+ if dd["ADDRESS"] in address_device_translation or dd.get("PARENT") in address_device_translation:
329
352
  new_devices.append(dd)
330
353
  else:
331
354
  new_devices.append(dd)
@@ -335,7 +358,7 @@ def get_xml_rpc_proxy( # noqa: C901
335
358
  async def ping(self, callerId: str) -> None:
336
359
  """Answer ping with pong."""
337
360
  if self._central:
338
- await self._central.data_point_event(
361
+ await self._central.event_coordinator.data_point_event(
339
362
  interface_id=callerId,
340
363
  channel_address="",
341
364
  parameter=Parameter.PONG,
@@ -347,17 +370,17 @@ def get_xml_rpc_proxy( # noqa: C901
347
370
  ) -> None:
348
371
  """Set a paramset."""
349
372
  if self._central and paramset_key == ParamsetKey.VALUES:
350
- interface_id = self._central.primary_client.interface_id # type: ignore[union-attr]
373
+ interface_id = self._central.client_coordinator.primary_client.interface_id # type: ignore[union-attr]
351
374
  for param, value in values.items():
352
- await self._central.data_point_event(
375
+ await self._central.event_coordinator.data_point_event(
353
376
  interface_id=interface_id, channel_address=channel_address, parameter=param, value=value
354
377
  )
355
378
 
356
379
  async def setValue(self, channel_address: str, parameter: str, value: Any, rx_mode: Any | None = None) -> None:
357
380
  """Set a value."""
358
381
  if self._central:
359
- await self._central.data_point_event(
360
- interface_id=self._central.primary_client.interface_id, # type: ignore[union-attr]
382
+ await self._central.event_coordinator.data_point_event(
383
+ interface_id=self._central.client_coordinator.primary_client.interface_id, # type: ignore[union-attr]
361
384
  channel_address=channel_address,
362
385
  parameter=parameter,
363
386
  value=value,
@@ -381,7 +404,7 @@ def get_xml_rpc_proxy( # noqa: C901
381
404
  return cast(BaseRpcProxy, _AioXmlRpcProxyFromSession())
382
405
 
383
406
 
384
- def _get_instance_attributes(instance: Any) -> set[str]:
407
+ def _get_instance_attributes(instance: Any) -> set[str]: # kwonly: disable
385
408
  """
386
409
  Get all instance attribute names, supporting both __dict__ and __slots__.
387
410
 
@@ -478,9 +501,11 @@ def get_mock(
478
501
  setattr(instance, attr, getattr(instance._mock_wraps, attr))
479
502
  return instance
480
503
 
481
- # Step 1: Identify all @property decorated attributes on the class
504
+ # Step 1: Identify all @property and DelegatedProperty decorated attributes on the class
482
505
  # These need special handling because MagicMock doesn't delegate property access
483
506
  property_names = _get_properties(data_object=instance, decorator=property)
507
+ # Also include DelegatedProperty descriptors which are now used extensively
508
+ property_names |= _get_properties(data_object=instance, decorator=DelegatedProperty)
484
509
 
485
510
  # Step 2: Create a dynamic MagicMock subclass
486
511
  # We add property descriptors to this subclass that delegate to _mock_wraps.
@@ -545,15 +570,15 @@ def get_mock(
545
570
  # Step 6: Copy non-mockable methods directly
546
571
  # Some methods (like bound methods or special attributes) need to be copied
547
572
  # directly rather than being mocked
548
- try:
549
- for method_name in [
550
- prop
551
- for prop in _get_not_mockable_method_names(instance=instance, exclude_methods=exclude_methods)
552
- if prop not in include_properties and prop not in kwargs and prop not in property_names
553
- ]:
573
+ for method_name in [
574
+ prop
575
+ for prop in _get_not_mockable_method_names(instance=instance, exclude_methods=exclude_methods)
576
+ if prop not in include_properties and prop not in kwargs and prop not in property_names
577
+ ]:
578
+ try:
554
579
  setattr(mock, method_name, getattr(instance, method_name))
555
- except Exception:
556
- pass
580
+ except (AttributeError, TypeError) as exc:
581
+ _LOGGER.debug("Could not copy method %s to mock: %s", method_name, exc)
557
582
 
558
583
  return mock
559
584
 
@@ -581,6 +606,26 @@ class SessionPlayer:
581
606
  """Initialize the session player."""
582
607
  self._file_id = file_id
583
608
 
609
+ @classmethod
610
+ def clear_all(cls) -> None:
611
+ """Clear all cached session data from all file IDs."""
612
+ cls._store.clear()
613
+
614
+ @classmethod
615
+ def clear_file(cls, *, file_id: str) -> None:
616
+ """Clear cached session data for a specific file ID."""
617
+ cls._store.pop(file_id, None)
618
+
619
+ @classmethod
620
+ def get_loaded_file_ids(cls) -> list[str]:
621
+ """Return list of currently loaded file IDs."""
622
+ return list(cls._store.keys())
623
+
624
+ @classmethod
625
+ def get_memory_usage(cls) -> int:
626
+ """Return approximate memory usage of cached session data in bytes."""
627
+ return sys.getsizeof(cls._store)
628
+
584
629
  @property
585
630
  def _secondary_file_ids(self) -> list[str]:
586
631
  """Return the secondary store for the given file_id."""
@@ -623,7 +668,7 @@ class SessionPlayer:
623
668
  except ValueError:
624
669
  continue
625
670
  resp = bucket_by_ts[latest_ts]
626
- params = _unfreeze_params(frozen_params=frozen_params)
671
+ params = unfreeze_params(frozen_params=frozen_params)
627
672
 
628
673
  result.append((params, resp))
629
674
  return result
@@ -668,7 +713,7 @@ class SessionPlayer:
668
713
  return None
669
714
  if not (bucket_by_parameter := bucket_by_method.get(method)):
670
715
  return None
671
- frozen_params = _freeze_params(params=_cleanup_params_for_session(params=params))
716
+ frozen_params = freeze_params(params=cleanup_params_for_session(params=params))
672
717
 
673
718
  # For each parameter, choose the response at the latest timestamp.
674
719
  if (bucket_by_ts := bucket_by_parameter.get(frozen_params)) is None:
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic-test-support
3
- Version: 2025.12.23
3
+ Version: 2026.1.23
4
4
  Summary: Support-only package for AioHomematic (tests/dev). Not part of production builds.
5
- Author-email: SukramJ <sukramj@icloud.com>
5
+ Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/SukramJ/aiohomematic
7
7
  Classifier: Development Status :: 3 - Alpha
8
8
  Classifier: Intended Audience :: Developers
@@ -0,0 +1,15 @@
1
+ aiohomematic_test_support/__init__.py,sha256=xZticN_kTORdNFQgnUH4pA57HNPUKUYQ8Kw0z2xpNtY,2711
2
+ aiohomematic_test_support/const.py,sha256=cYsT9WMnoxRR1zRRUFX8M7A9e0pwS3NcxKK9smXiiw8,6487
3
+ aiohomematic_test_support/event_capture.py,sha256=9giXt8-vbPXUbG0X89Jt5Q8LfPli0y-liLVVUhiw-EM,8002
4
+ aiohomematic_test_support/event_mock.py,sha256=sWtAqMPXxkWFNqslw2wfgXGtSM0kLMm0Lg_qkkAFE4g,8493
5
+ aiohomematic_test_support/factory.py,sha256=B3R_FxZP6zRi3A-CqUmcAjkqTX9GIm-kSEKLx2OW9vE,12472
6
+ aiohomematic_test_support/helper.py,sha256=r63EpCEPmWFdcxiEDR5k22AwbB112DbWJwK66iHLeU4,1468
7
+ aiohomematic_test_support/mock.py,sha256=GYwpwH-mfaobK8DyNj3FFC2zyvATs46sx2l7V5wnqGA,32366
8
+ aiohomematic_test_support/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ aiohomematic_test_support/data/device_translation.json,sha256=owHLpCGvtMJzfCMOMMgnn7-LgN-BMKSYzi4NKINw1WQ,13290
10
+ aiohomematic_test_support/data/full_session_randomized_ccu.zip,sha256=kKHMtJouCskAHkXaP1hHRSoWZO2cZDJ9P_EwuJuywJQ,733017
11
+ aiohomematic_test_support/data/full_session_randomized_pydevccu.zip,sha256=_QFWSP03dkiMFdD_w-R98DS6ur4PYDQXw-DCkbJEGg4,1293240
12
+ aiohomematic_test_support-2026.1.23.dist-info/METADATA,sha256=BGjkXtFTGXYKzmYdb1bMT9TMt3A_AszRMsQSa_FN0FM,575
13
+ aiohomematic_test_support-2026.1.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ aiohomematic_test_support-2026.1.23.dist-info/top_level.txt,sha256=KmK-OiDDbrmawIsIgPWNAkpkDfWQnOoumYd9MXAiTHc,26
15
+ aiohomematic_test_support-2026.1.23.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- aiohomematic_test_support/__init__.py,sha256=hwFQdytuqaQAqUbDFz_hSF39zeCY1PW_vafTdwa-Qoo,1623
2
- aiohomematic_test_support/const.py,sha256=YXN3FvZCqjxOaqCs0E7KTz2PUa1zONzS9-W_6YqJrPI,19755
3
- aiohomematic_test_support/factory.py,sha256=E3tHyeIAO1Id1c0ODP7WnUs_XOgnt14bMzPyow5SEJc,10772
4
- aiohomematic_test_support/helper.py,sha256=Ue2tfy10_fiuMjYsc1jYPvo5sEtMF2WVKjvLnTZ0TzU,1360
5
- aiohomematic_test_support/mock.py,sha256=h7kL6R7IeZfRArZ7TXfVgX_O2GRC-7CQk9llysQczTI,30228
6
- aiohomematic_test_support/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- aiohomematic_test_support/data/full_session_randomized_ccu.zip,sha256=kKHMtJouCskAHkXaP1hHRSoWZO2cZDJ9P_EwuJuywJQ,733017
8
- aiohomematic_test_support/data/full_session_randomized_pydevccu.zip,sha256=_QFWSP03dkiMFdD_w-R98DS6ur4PYDQXw-DCkbJEGg4,1293240
9
- aiohomematic_test_support-2025.12.23.dist-info/METADATA,sha256=4MzyPgvkaZ98FmvnyIaBgIjGw1Vc1gFDcu06LzacPMc,536
10
- aiohomematic_test_support-2025.12.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- aiohomematic_test_support-2025.12.23.dist-info/top_level.txt,sha256=KmK-OiDDbrmawIsIgPWNAkpkDfWQnOoumYd9MXAiTHc,26
12
- aiohomematic_test_support-2025.12.23.dist-info/RECORD,,