aiohomematic-test-support 2025.10.17__py3-none-any.whl → 2025.10.18__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-test-support might be problematic. Click here for more details.
- aiohomematic_test_support/__init__.py +1 -1
- aiohomematic_test_support/const.py +5 -0
- aiohomematic_test_support/factory.py +2 -0
- aiohomematic_test_support/mock.py +90 -22
- {aiohomematic_test_support-2025.10.17.dist-info → aiohomematic_test_support-2025.10.18.dist-info}/METADATA +1 -1
- aiohomematic_test_support-2025.10.18.dist-info/RECORD +12 -0
- aiohomematic_test_support-2025.10.17.dist-info/RECORD +0 -12
- {aiohomematic_test_support-2025.10.17.dist-info → aiohomematic_test_support-2025.10.18.dist-info}/WHEEL +0 -0
- {aiohomematic_test_support-2025.10.17.dist-info → aiohomematic_test_support-2025.10.18.dist-info}/top_level.txt +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "2025.10.
|
|
1
|
+
__version__ = "2025.10.18"
|
|
2
2
|
"""Module to support aiohomematic testing with a local client."""
|
|
@@ -16,6 +16,11 @@ INTERFACE_ID = f"{CENTRAL_NAME}-{Interface.BIDCOS_RF}"
|
|
|
16
16
|
FULL_SESSION_RANDOMIZED_PYDEVCCU = "full_session_randomized_pydevccu.zip"
|
|
17
17
|
FULL_SESSION_RANDOMIZED_CCU = "full_session_randomized_ccu.zip"
|
|
18
18
|
|
|
19
|
+
ALL_SESSION_FILES = [
|
|
20
|
+
FULL_SESSION_RANDOMIZED_PYDEVCCU,
|
|
21
|
+
FULL_SESSION_RANDOMIZED_CCU,
|
|
22
|
+
]
|
|
23
|
+
|
|
19
24
|
|
|
20
25
|
SYSVAR_DATA: list[SystemVariableData] = [
|
|
21
26
|
SystemVariableData(
|
|
@@ -152,6 +152,7 @@ class FactoryWithClient:
|
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
async def get_central_client_factory(
|
|
155
|
+
*,
|
|
155
156
|
player: SessionPlayer,
|
|
156
157
|
address_device_translation: set[str],
|
|
157
158
|
do_mock_client: bool,
|
|
@@ -179,6 +180,7 @@ async def get_central_client_factory(
|
|
|
179
180
|
|
|
180
181
|
|
|
181
182
|
async def get_pydev_ccu_central_unit_full(
|
|
183
|
+
*,
|
|
182
184
|
port: int,
|
|
183
185
|
client_session: ClientSession | None = None,
|
|
184
186
|
) -> CentralUnit:
|
|
@@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
27
27
|
# pylint: disable=protected-access
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def _get_not_mockable_method_names(instance: Any, exclude_methods: set[str]) -> set[str]:
|
|
30
|
+
def _get_not_mockable_method_names(*, instance: Any, exclude_methods: set[str]) -> set[str]:
|
|
31
31
|
"""Return all relevant method names for mocking."""
|
|
32
32
|
methods: set[str] = set(_get_properties(data_object=instance, decorator=property))
|
|
33
33
|
|
|
@@ -37,7 +37,7 @@ def _get_not_mockable_method_names(instance: Any, exclude_methods: set[str]) ->
|
|
|
37
37
|
return methods
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def _get_properties(data_object: Any, decorator: Any) -> set[str]:
|
|
40
|
+
def _get_properties(*, data_object: Any, decorator: Any) -> set[str]:
|
|
41
41
|
"""Return the object attributes by decorator."""
|
|
42
42
|
cls = data_object.__class__
|
|
43
43
|
|
|
@@ -63,7 +63,7 @@ def get_client_session( # noqa: C901
|
|
|
63
63
|
"""
|
|
64
64
|
|
|
65
65
|
class _MockResponse:
|
|
66
|
-
def __init__(self, json_data: dict | None) -> None:
|
|
66
|
+
def __init__(self, *, json_data: dict | None) -> None:
|
|
67
67
|
# If no match is found, emulate backend error payload
|
|
68
68
|
self._json = json_data or {
|
|
69
69
|
_JsonKey.RESULT: None,
|
|
@@ -72,7 +72,7 @@ def get_client_session( # noqa: C901
|
|
|
72
72
|
}
|
|
73
73
|
self.status = 200
|
|
74
74
|
|
|
75
|
-
async def json(self, encoding: str | None = None) -> dict[str, Any]: # mimic aiohttp API
|
|
75
|
+
async def json(self, *, encoding: str | None = None) -> dict[str, Any]: # mimic aiohttp API
|
|
76
76
|
return self._json
|
|
77
77
|
|
|
78
78
|
async def read(self) -> bytes:
|
|
@@ -114,24 +114,32 @@ def get_client_session( # noqa: C901
|
|
|
114
114
|
_JsonRpcMethod.SYSVAR_SET_FLOAT,
|
|
115
115
|
_JsonRpcMethod.SESSION_LOGOUT,
|
|
116
116
|
):
|
|
117
|
-
return _MockResponse({_JsonKey.ID: 0, _JsonKey.RESULT: "200", _JsonKey.ERROR: None})
|
|
117
|
+
return _MockResponse(json_data={_JsonKey.ID: 0, _JsonKey.RESULT: "200", _JsonKey.ERROR: None})
|
|
118
118
|
if method == _JsonRpcMethod.SYSVAR_GET_ALL:
|
|
119
119
|
return _MockResponse(
|
|
120
|
-
{_JsonKey.ID: 0, _JsonKey.RESULT: const.SYSVAR_DATA_JSON, _JsonKey.ERROR: None}
|
|
120
|
+
json_data={_JsonKey.ID: 0, _JsonKey.RESULT: const.SYSVAR_DATA_JSON, _JsonKey.ERROR: None}
|
|
121
121
|
)
|
|
122
122
|
if method == _JsonRpcMethod.PROGRAM_GET_ALL:
|
|
123
123
|
return _MockResponse(
|
|
124
|
-
{_JsonKey.ID: 0, _JsonKey.RESULT: const.PROGRAM_DATA_JSON, _JsonKey.ERROR: None}
|
|
124
|
+
json_data={_JsonKey.ID: 0, _JsonKey.RESULT: const.PROGRAM_DATA_JSON, _JsonKey.ERROR: None}
|
|
125
125
|
)
|
|
126
126
|
if method == _JsonRpcMethod.REGA_RUN_SCRIPT:
|
|
127
127
|
if "get_program_descriptions" in params[_JsonKey.SCRIPT]:
|
|
128
128
|
return _MockResponse(
|
|
129
|
-
{
|
|
129
|
+
json_data={
|
|
130
|
+
_JsonKey.ID: 0,
|
|
131
|
+
_JsonKey.RESULT: const.PROGRAM_DATA_JSON_DESCRIPTION,
|
|
132
|
+
_JsonKey.ERROR: None,
|
|
133
|
+
}
|
|
130
134
|
)
|
|
131
135
|
|
|
132
136
|
if "get_system_variable_descriptions" in params[_JsonKey.SCRIPT]:
|
|
133
137
|
return _MockResponse(
|
|
134
|
-
{
|
|
138
|
+
json_data={
|
|
139
|
+
_JsonKey.ID: 0,
|
|
140
|
+
_JsonKey.RESULT: const.SYSVAR_DATA_JSON_DESCRIPTION,
|
|
141
|
+
_JsonKey.ERROR: None,
|
|
142
|
+
}
|
|
135
143
|
)
|
|
136
144
|
|
|
137
145
|
if method == _JsonRpcMethod.INTERFACE_SET_VALUE:
|
|
@@ -141,7 +149,7 @@ def get_client_session( # noqa: C901
|
|
|
141
149
|
parameter=params[_JsonKey.VALUE_KEY],
|
|
142
150
|
value=params[_JsonKey.VALUE],
|
|
143
151
|
)
|
|
144
|
-
return _MockResponse({_JsonKey.ID: 0, _JsonKey.RESULT: "200", _JsonKey.ERROR: None})
|
|
152
|
+
return _MockResponse(json_data={_JsonKey.ID: 0, _JsonKey.RESULT: "200", _JsonKey.ERROR: None})
|
|
145
153
|
if method == _JsonRpcMethod.INTERFACE_PUT_PARAMSET:
|
|
146
154
|
if params[_JsonKey.PARAMSET_KEY] == ParamsetKey.VALUES:
|
|
147
155
|
interface_id = params[_JsonKey.INTERFACE]
|
|
@@ -154,7 +162,7 @@ def get_client_session( # noqa: C901
|
|
|
154
162
|
parameter=param,
|
|
155
163
|
value=value,
|
|
156
164
|
)
|
|
157
|
-
return _MockResponse({_JsonKey.RESULT: "200", _JsonKey.ERROR: None})
|
|
165
|
+
return _MockResponse(json_data={_JsonKey.RESULT: "200", _JsonKey.ERROR: None})
|
|
158
166
|
|
|
159
167
|
json_data = player.get_latest_response_by_params(
|
|
160
168
|
rpc_type=RPCType.JSON_RPC,
|
|
@@ -177,7 +185,7 @@ def get_client_session( # noqa: C901
|
|
|
177
185
|
new_devices.append(dd)
|
|
178
186
|
|
|
179
187
|
json_data[_JsonKey.RESULT] = new_devices
|
|
180
|
-
return _MockResponse(json_data)
|
|
188
|
+
return _MockResponse(json_data=json_data)
|
|
181
189
|
|
|
182
190
|
async def close(self) -> None: # compatibility
|
|
183
191
|
return None
|
|
@@ -327,7 +335,7 @@ def get_xml_rpc_proxy( # noqa: C901
|
|
|
327
335
|
|
|
328
336
|
|
|
329
337
|
def get_mock(
|
|
330
|
-
instance: Any, exclude_methods: set[str] | None = None, include_properties: set[str] | None = None, **kwargs: Any
|
|
338
|
+
*, instance: Any, exclude_methods: set[str] | None = None, include_properties: set[str] | None = None, **kwargs: Any
|
|
331
339
|
) -> Any:
|
|
332
340
|
"""Create a mock and copy instance attributes over mock."""
|
|
333
341
|
if exclude_methods is None:
|
|
@@ -356,8 +364,12 @@ def get_mock(
|
|
|
356
364
|
async def get_session_player(*, file_name: str) -> SessionPlayer:
|
|
357
365
|
"""Provide a SessionPlayer preloaded from the randomized full session JSON file."""
|
|
358
366
|
player = SessionPlayer(file_id=file_name)
|
|
359
|
-
|
|
360
|
-
|
|
367
|
+
if player.supports_file_id(file_id=file_name):
|
|
368
|
+
return player
|
|
369
|
+
|
|
370
|
+
for load_fn in const.ALL_SESSION_FILES:
|
|
371
|
+
file_path = os.path.join(os.path.dirname(__file__), "data", load_fn)
|
|
372
|
+
await player.load(file_path=file_path, file_id=load_fn)
|
|
361
373
|
return player
|
|
362
374
|
|
|
363
375
|
|
|
@@ -372,7 +384,16 @@ class SessionPlayer:
|
|
|
372
384
|
"""Initialize the session player."""
|
|
373
385
|
self._file_id = file_id
|
|
374
386
|
|
|
375
|
-
|
|
387
|
+
@property
|
|
388
|
+
def _secondary_file_ids(self) -> list[str]:
|
|
389
|
+
"""Return the secondary store for the given file_id."""
|
|
390
|
+
return [fid for fid in self._store if fid != self._file_id]
|
|
391
|
+
|
|
392
|
+
def supports_file_id(self, *, file_id: str) -> bool:
|
|
393
|
+
"""Return whether the session player supports the given file_id."""
|
|
394
|
+
return file_id in self._store
|
|
395
|
+
|
|
396
|
+
async def load(self, *, file_path: str, file_id: str) -> DataOperationResult:
|
|
376
397
|
"""
|
|
377
398
|
Load data from disk into the dictionary.
|
|
378
399
|
|
|
@@ -381,7 +402,7 @@ class SessionPlayer:
|
|
|
381
402
|
will be loaded.
|
|
382
403
|
"""
|
|
383
404
|
|
|
384
|
-
if self.
|
|
405
|
+
if self.supports_file_id(file_id=file_id):
|
|
385
406
|
return DataOperationResult.NO_LOAD
|
|
386
407
|
|
|
387
408
|
if not os.path.exists(file_path):
|
|
@@ -400,7 +421,7 @@ class SessionPlayer:
|
|
|
400
421
|
with open(file=file_path, encoding=UTF_8) as file_pointer:
|
|
401
422
|
data = json.loads(file_pointer.read())
|
|
402
423
|
|
|
403
|
-
self._store[
|
|
424
|
+
self._store[file_id] = data
|
|
404
425
|
except (json.JSONDecodeError, zipfile.BadZipFile, UnicodeDecodeError, OSError):
|
|
405
426
|
return DataOperationResult.LOAD_FAIL
|
|
406
427
|
return DataOperationResult.LOAD_SUCCESS
|
|
@@ -408,11 +429,13 @@ class SessionPlayer:
|
|
|
408
429
|
loop = asyncio.get_running_loop()
|
|
409
430
|
return await loop.run_in_executor(None, _perform_load)
|
|
410
431
|
|
|
411
|
-
def
|
|
432
|
+
def get_latest_response_by_method_for_file_id(
|
|
433
|
+
self, *, file_id: str, rpc_type: str, method: str
|
|
434
|
+
) -> list[tuple[Any, Any]]:
|
|
412
435
|
"""Return latest non-expired responses for a given (rpc_type, method)."""
|
|
413
436
|
result: list[Any] = []
|
|
414
437
|
# Access store safely to avoid side effects from creating buckets.
|
|
415
|
-
if not (bucket_by_method := self._store[
|
|
438
|
+
if not (bucket_by_method := self._store[file_id].get(rpc_type)):
|
|
416
439
|
return result
|
|
417
440
|
if not (bucket_by_parameter := bucket_by_method.get(method)):
|
|
418
441
|
return result
|
|
@@ -430,16 +453,35 @@ class SessionPlayer:
|
|
|
430
453
|
result.append((params, resp))
|
|
431
454
|
return result
|
|
432
455
|
|
|
433
|
-
def
|
|
456
|
+
def get_latest_response_by_method(self, *, rpc_type: str, method: str) -> list[tuple[Any, Any]]:
|
|
457
|
+
"""Return latest non-expired responses for a given (rpc_type, method)."""
|
|
458
|
+
if pri_result := self.get_latest_response_by_method_for_file_id(
|
|
459
|
+
file_id=self._file_id,
|
|
460
|
+
rpc_type=rpc_type,
|
|
461
|
+
method=method,
|
|
462
|
+
):
|
|
463
|
+
return pri_result
|
|
464
|
+
|
|
465
|
+
for secondary_file_id in self._secondary_file_ids:
|
|
466
|
+
if sec_result := self.get_latest_response_by_method_for_file_id(
|
|
467
|
+
file_id=secondary_file_id,
|
|
468
|
+
rpc_type=rpc_type,
|
|
469
|
+
method=method,
|
|
470
|
+
):
|
|
471
|
+
return sec_result
|
|
472
|
+
return pri_result
|
|
473
|
+
|
|
474
|
+
def get_latest_response_by_params_for_file_id(
|
|
434
475
|
self,
|
|
435
476
|
*,
|
|
477
|
+
file_id: str,
|
|
436
478
|
rpc_type: str,
|
|
437
479
|
method: str,
|
|
438
480
|
params: Any,
|
|
439
481
|
) -> Any:
|
|
440
482
|
"""Return latest non-expired responses for a given (rpc_type, method, params)."""
|
|
441
483
|
# Access store safely to avoid side effects from creating buckets.
|
|
442
|
-
if not (bucket_by_method := self._store[
|
|
484
|
+
if not (bucket_by_method := self._store[file_id].get(rpc_type)):
|
|
443
485
|
return None
|
|
444
486
|
if not (bucket_by_parameter := bucket_by_method.get(method)):
|
|
445
487
|
return None
|
|
@@ -454,3 +496,29 @@ class SessionPlayer:
|
|
|
454
496
|
return bucket_by_ts[latest_ts]
|
|
455
497
|
except ValueError:
|
|
456
498
|
return None
|
|
499
|
+
|
|
500
|
+
def get_latest_response_by_params(
|
|
501
|
+
self,
|
|
502
|
+
*,
|
|
503
|
+
rpc_type: str,
|
|
504
|
+
method: str,
|
|
505
|
+
params: Any,
|
|
506
|
+
) -> Any:
|
|
507
|
+
"""Return latest non-expired responses for a given (rpc_type, method, params)."""
|
|
508
|
+
if pri_result := self.get_latest_response_by_params_for_file_id(
|
|
509
|
+
file_id=self._file_id,
|
|
510
|
+
rpc_type=rpc_type,
|
|
511
|
+
method=method,
|
|
512
|
+
params=params,
|
|
513
|
+
):
|
|
514
|
+
return pri_result
|
|
515
|
+
|
|
516
|
+
for secondary_file_id in self._secondary_file_ids:
|
|
517
|
+
if sec_result := self.get_latest_response_by_params_for_file_id(
|
|
518
|
+
file_id=secondary_file_id,
|
|
519
|
+
rpc_type=rpc_type,
|
|
520
|
+
method=method,
|
|
521
|
+
params=params,
|
|
522
|
+
):
|
|
523
|
+
return sec_result
|
|
524
|
+
return pri_result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic-test-support
|
|
3
|
-
Version: 2025.10.
|
|
3
|
+
Version: 2025.10.18
|
|
4
4
|
Summary: Support-only package for AioHomematic (tests/dev). Not part of production builds.
|
|
5
5
|
Author-email: SukramJ <sukramj@icloud.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/SukramJ/aiohomematic
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
aiohomematic_test_support/__init__.py,sha256=vd4sacVZP3Omyq1nAHonJJ-mE7chdIn9ue0j2NNSUmY,93
|
|
2
|
+
aiohomematic_test_support/const.py,sha256=4AGTK0gMaXPeje0Z2Hy9q11kxKW8TxiD5nIAygypXc0,19564
|
|
3
|
+
aiohomematic_test_support/factory.py,sha256=fzkhkI4LqHbzlH0GkwpAWgRjAnLpKd0dbq3APiEecFk,8541
|
|
4
|
+
aiohomematic_test_support/helper.py,sha256=pE687jG-KkrXZUFXnHTiydmhAvlGTjlejQoPeaH9w5w,1304
|
|
5
|
+
aiohomematic_test_support/mock.py,sha256=E3lMlLR_by2ZLRV_zJtI0Ru-B7adLNp9YY43vBPi8_E,21023
|
|
6
|
+
aiohomematic_test_support/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
aiohomematic_test_support/data/full_session_randomized_ccu.zip,sha256=oN7g0CB_0kQX3qk-RSu-Rt28yW7BcplVqPYZCDqr0EU,734626
|
|
8
|
+
aiohomematic_test_support/data/full_session_randomized_pydevccu.zip,sha256=_QFWSP03dkiMFdD_w-R98DS6ur4PYDQXw-DCkbJEGg4,1293240
|
|
9
|
+
aiohomematic_test_support-2025.10.18.dist-info/METADATA,sha256=w_d31xurGNK8VhWJIc9zjEP_Z6p4HFcJBPq0_9qIqls,536
|
|
10
|
+
aiohomematic_test_support-2025.10.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
aiohomematic_test_support-2025.10.18.dist-info/top_level.txt,sha256=KmK-OiDDbrmawIsIgPWNAkpkDfWQnOoumYd9MXAiTHc,26
|
|
12
|
+
aiohomematic_test_support-2025.10.18.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
aiohomematic_test_support/__init__.py,sha256=nSOvHSpyjyQVPzlPetRrLDdAs8dP_udy7R3slRnKMqk,93
|
|
2
|
-
aiohomematic_test_support/const.py,sha256=vatushPkwtueWUMdGkEJMTU6UB0Enxy4u7IXjUrXmJo,19468
|
|
3
|
-
aiohomematic_test_support/factory.py,sha256=oxg1TVafz59IbDloV__8LppXFm-O1bUz9U_50HmCkgs,8527
|
|
4
|
-
aiohomematic_test_support/helper.py,sha256=pE687jG-KkrXZUFXnHTiydmhAvlGTjlejQoPeaH9w5w,1304
|
|
5
|
-
aiohomematic_test_support/mock.py,sha256=jt2fcQyQGU_VSRDfsiSGW3yKUJrXBiB5HQz4Rue0Rac,18552
|
|
6
|
-
aiohomematic_test_support/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
aiohomematic_test_support/data/full_session_randomized_ccu.zip,sha256=oN7g0CB_0kQX3qk-RSu-Rt28yW7BcplVqPYZCDqr0EU,734626
|
|
8
|
-
aiohomematic_test_support/data/full_session_randomized_pydevccu.zip,sha256=_QFWSP03dkiMFdD_w-R98DS6ur4PYDQXw-DCkbJEGg4,1293240
|
|
9
|
-
aiohomematic_test_support-2025.10.17.dist-info/METADATA,sha256=nc_SLXIcz8eHyejdzTrEzgBNY6DClnI3lpCWuey38Eg,536
|
|
10
|
-
aiohomematic_test_support-2025.10.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
-
aiohomematic_test_support-2025.10.17.dist-info/top_level.txt,sha256=KmK-OiDDbrmawIsIgPWNAkpkDfWQnOoumYd9MXAiTHc,26
|
|
12
|
-
aiohomematic_test_support-2025.10.17.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|