aiohomematic-test-support 2025.10.17__py3-none-any.whl → 2025.10.20b0__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.

@@ -1,2 +1,2 @@
1
- __version__ = "2025.10.17"
1
+ __version__ = "2025.10.20b"
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(
@@ -1,4 +1,4 @@
1
- """Helpers for tests."""
1
+ """Factories for tests."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -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:
@@ -1,4 +1,4 @@
1
- """Helpers for tests."""
1
+ """Mocks for tests."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -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
- {_JsonKey.ID: 0, _JsonKey.RESULT: const.PROGRAM_DATA_JSON_DESCRIPTION, _JsonKey.ERROR: None}
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
- {_JsonKey.ID: 0, _JsonKey.RESULT: const.SYSVAR_DATA_JSON_DESCRIPTION, _JsonKey.ERROR: None}
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
- file_path = os.path.join(os.path.dirname(__file__), "data", file_name)
360
- await player.load(file_path=file_path)
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
- async def load(self, *, file_path: str) -> DataOperationResult:
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._store[self._file_id]:
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[self._file_id] = data
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 get_latest_response_by_method(self, *, rpc_type: str, method: str) -> list[tuple[Any, Any]]:
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[self._file_id].get(rpc_type)):
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 get_latest_response_by_params(
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[self._file_id].get(rpc_type)):
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.17
3
+ Version: 2025.10.20b0
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=6h4Q0p1lxNJ_LAKFBU6Maud5e8_2Vi6nkg-cSGqADsY,94
2
+ aiohomematic_test_support/const.py,sha256=4AGTK0gMaXPeje0Z2Hy9q11kxKW8TxiD5nIAygypXc0,19564
3
+ aiohomematic_test_support/factory.py,sha256=6WKg6fCmB81jZh7QZHi366VMx5Yy0a5TWJsv-m7m1M8,8543
4
+ aiohomematic_test_support/helper.py,sha256=pE687jG-KkrXZUFXnHTiydmhAvlGTjlejQoPeaH9w5w,1304
5
+ aiohomematic_test_support/mock.py,sha256=TGnvNlqIVTl7Fzhdwx3OgcMltN0MCmHLd7LbqgSelv8,21021
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.20b0.dist-info/METADATA,sha256=aIcb4hLNG1TYpOhiL1kSolOJbKmu-7NZarbJ2WBCMqc,538
10
+ aiohomematic_test_support-2025.10.20b0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ aiohomematic_test_support-2025.10.20b0.dist-info/top_level.txt,sha256=KmK-OiDDbrmawIsIgPWNAkpkDfWQnOoumYd9MXAiTHc,26
12
+ aiohomematic_test_support-2025.10.20b0.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,,