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.
- aiohomematic_test_support/__init__.py +33 -2
- aiohomematic_test_support/const.py +26 -365
- aiohomematic_test_support/data/device_translation.json +364 -0
- aiohomematic_test_support/event_capture.py +257 -0
- aiohomematic_test_support/event_mock.py +300 -0
- aiohomematic_test_support/factory.py +77 -29
- aiohomematic_test_support/helper.py +6 -4
- aiohomematic_test_support/mock.py +68 -23
- {aiohomematic_test_support-2025.12.23.dist-info → aiohomematic_test_support-2026.1.23.dist-info}/METADATA +2 -2
- aiohomematic_test_support-2026.1.23.dist-info/RECORD +15 -0
- aiohomematic_test_support-2025.12.23.dist-info/RECORD +0 -12
- {aiohomematic_test_support-2025.12.23.dist-info → aiohomematic_test_support-2026.1.23.dist-info}/WHEEL +0 -0
- {aiohomematic_test_support-2025.12.23.dist-info → aiohomematic_test_support-2026.1.23.dist-info}/top_level.txt +0 -0
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
556
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
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,,
|
|
File without changes
|
|
File without changes
|