aiohomematic-test-support 2025.11.3__tar.gz → 2025.11.11__tar.gz

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.
Files changed (17) hide show
  1. {aiohomematic_test_support-2025.11.3/aiohomematic_test_support.egg-info → aiohomematic_test_support-2025.11.11}/PKG-INFO +1 -1
  2. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/__init__.py +1 -1
  3. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11/aiohomematic_test_support.egg-info}/PKG-INFO +1 -1
  4. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/mock.py +128 -128
  5. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/MANIFEST.in +0 -0
  6. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/README.md +0 -0
  7. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/aiohomematic_test_support.egg-info/SOURCES.txt +0 -0
  8. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/aiohomematic_test_support.egg-info/dependency_links.txt +0 -0
  9. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/aiohomematic_test_support.egg-info/top_level.txt +0 -0
  10. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/const.py +0 -0
  11. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/data/full_session_randomized_ccu.zip +0 -0
  12. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/data/full_session_randomized_pydevccu.zip +0 -0
  13. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/factory.py +56 -56
  14. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/helper.py +0 -0
  15. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/py.typed +0 -0
  16. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/pyproject.toml +0 -0
  17. {aiohomematic_test_support-2025.11.3 → aiohomematic_test_support-2025.11.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic-test-support
3
- Version: 2025.11.3
3
+ Version: 2025.11.11
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
@@ -1,2 +1,2 @@
1
- __version__ = "2025.11.3"
1
+ __version__ = "2025.11.11"
2
2
  """Module to support aiohomematic testing with a local client."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic-test-support
3
- Version: 2025.11.3
3
+ Version: 2025.11.11
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
@@ -63,9 +63,9 @@ 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[str, Any] | None) -> None:
67
67
  # If no match is found, emulate backend error payload
68
- self._json = json_data or {
68
+ self._json: dict[str, Any] = json_data or {
69
69
  _JsonKey.RESULT: None,
70
70
  _JsonKey.ERROR: {"name": "-1", "code": -1, "message": "Not found in session player"},
71
71
  _JsonKey.ID: 0,
@@ -83,9 +83,8 @@ def get_client_session( # noqa: C901
83
83
  """Initialize the mock client session."""
84
84
  self._central: CentralUnit | None = None
85
85
 
86
- def set_central(self, *, central: CentralUnit) -> None:
87
- """Set the central."""
88
- self._central = central
86
+ async def close(self) -> None: # compatibility
87
+ return None
89
88
 
90
89
  async def post(
91
90
  self,
@@ -187,8 +186,9 @@ def get_client_session( # noqa: C901
187
186
  json_data[_JsonKey.RESULT] = new_devices
188
187
  return _MockResponse(json_data=json_data)
189
188
 
190
- async def close(self) -> None: # compatibility
191
- return None
189
+ def set_central(self, *, central: CentralUnit) -> None:
190
+ """Set the central."""
191
+ self._central = central
192
192
 
193
193
  return cast(ClientSession, _MockClientSession())
194
194
 
@@ -212,29 +212,40 @@ def get_xml_rpc_proxy( # noqa: C901
212
212
  self._name = full_name
213
213
  self._caller = caller
214
214
 
215
- def __getattr__(self, sub: str) -> _Method:
216
- # Allow chaining like proxy.system.listMethods
217
- return _Method(f"{self._name}.{sub}", self._caller)
218
-
219
215
  async def __call__(self, *args: Any) -> Any:
220
216
  # Forward to caller with collected method name and positional params
221
217
  return await self._caller(self._name, *args)
222
218
 
219
+ def __getattr__(self, sub: str) -> _Method:
220
+ # Allow chaining like proxy.system.listMethods
221
+ return _Method(f"{self._name}.{sub}", self._caller)
222
+
223
223
  class _AioXmlRpcProxyFromSession:
224
224
  def __init__(self) -> None:
225
225
  self._player = player
226
226
  self._supported_methods: tuple[str, ...] = ()
227
227
  self._central: CentralUnit | None = None
228
228
 
229
- def set_central(self, *, central: CentralUnit) -> None:
230
- """Set the central."""
231
- self._central = central
229
+ def __getattr__(self, name: str) -> Any:
230
+ # Start of method chain
231
+ return _Method(name, self._invoke)
232
232
 
233
233
  @property
234
234
  def supported_methods(self) -> tuple[str, ...]:
235
235
  """Return the supported methods."""
236
236
  return self._supported_methods
237
237
 
238
+ async def clientServerInitialized(self, interface_id: str) -> None:
239
+ """Answer clientServerInitialized with pong."""
240
+ await self.ping(callerId=interface_id)
241
+
242
+ async def do_init(self) -> None:
243
+ """Init the xml rpc proxy."""
244
+ if supported_methods := await self.system.listMethods():
245
+ # ping is missing in VirtualDevices interface but can be used.
246
+ supported_methods.append(_RpcMethod.PING)
247
+ self._supported_methods = tuple(supported_methods)
248
+
238
249
  async def getAllSystemVariables(self) -> dict[str, Any]:
239
250
  """Return all system variables."""
240
251
  return const.SYSVAR_DATA_XML
@@ -249,41 +260,6 @@ def get_xml_rpc_proxy( # noqa: C901
249
260
  )
250
261
  return result if result else {}
251
262
 
252
- async def setValue(self, channel_address: str, parameter: str, value: Any, rx_mode: Any | None = None) -> None:
253
- """Set a value."""
254
- if self._central:
255
- await self._central.data_point_event(
256
- interface_id=self._central.primary_client.interface_id, # type: ignore[union-attr]
257
- channel_address=channel_address,
258
- parameter=parameter,
259
- value=value,
260
- )
261
-
262
- async def putParamset(
263
- self, channel_address: str, paramset_key: str, values: Any, rx_mode: Any | None = None
264
- ) -> None:
265
- """Set a paramset."""
266
- if self._central and paramset_key == ParamsetKey.VALUES:
267
- interface_id = self._central.primary_client.interface_id # type: ignore[union-attr]
268
- for param, value in values.items():
269
- await self._central.data_point_event(
270
- interface_id=interface_id, channel_address=channel_address, parameter=param, value=value
271
- )
272
-
273
- async def ping(self, callerId: str) -> None:
274
- """Answer ping with pong."""
275
- if self._central:
276
- await self._central.data_point_event(
277
- interface_id=callerId,
278
- channel_address="",
279
- parameter=Parameter.PONG,
280
- value=callerId,
281
- )
282
-
283
- async def clientServerInitialized(self, interface_id: str) -> None:
284
- """Answer clientServerInitialized with pong."""
285
- await self.ping(callerId=interface_id)
286
-
287
263
  async def listDevices(self) -> list[Any]:
288
264
  """Return a list of devices."""
289
265
  devices = self._player.get_latest_response_by_params(
@@ -309,9 +285,43 @@ def get_xml_rpc_proxy( # noqa: C901
309
285
 
310
286
  return new_devices
311
287
 
312
- def __getattr__(self, name: str) -> Any:
313
- # Start of method chain
314
- return _Method(name, self._invoke)
288
+ async def ping(self, callerId: str) -> None:
289
+ """Answer ping with pong."""
290
+ if self._central:
291
+ await self._central.data_point_event(
292
+ interface_id=callerId,
293
+ channel_address="",
294
+ parameter=Parameter.PONG,
295
+ value=callerId,
296
+ )
297
+
298
+ async def putParamset(
299
+ self, channel_address: str, paramset_key: str, values: Any, rx_mode: Any | None = None
300
+ ) -> None:
301
+ """Set a paramset."""
302
+ if self._central and paramset_key == ParamsetKey.VALUES:
303
+ interface_id = self._central.primary_client.interface_id # type: ignore[union-attr]
304
+ for param, value in values.items():
305
+ await self._central.data_point_event(
306
+ interface_id=interface_id, channel_address=channel_address, parameter=param, value=value
307
+ )
308
+
309
+ async def setValue(self, channel_address: str, parameter: str, value: Any, rx_mode: Any | None = None) -> None:
310
+ """Set a value."""
311
+ if self._central:
312
+ await self._central.data_point_event(
313
+ interface_id=self._central.primary_client.interface_id, # type: ignore[union-attr]
314
+ channel_address=channel_address,
315
+ parameter=parameter,
316
+ value=value,
317
+ )
318
+
319
+ def set_central(self, *, central: CentralUnit) -> None:
320
+ """Set the central."""
321
+ self._central = central
322
+
323
+ async def stop(self) -> None: # compatibility with AioXmlRpcProxy.stop
324
+ return None
315
325
 
316
326
  async def _invoke(self, method: str, *args: Any) -> Any:
317
327
  params = tuple(args)
@@ -321,16 +331,6 @@ def get_xml_rpc_proxy( # noqa: C901
321
331
  params=params,
322
332
  )
323
333
 
324
- async def stop(self) -> None: # compatibility with AioXmlRpcProxy.stop
325
- return None
326
-
327
- async def do_init(self) -> None:
328
- """Init the xml rpc proxy."""
329
- if supported_methods := await self.system.listMethods():
330
- # ping is missing in VirtualDevices interface but can be used.
331
- supported_methods.append(_RpcMethod.PING)
332
- self._supported_methods = tuple(supported_methods)
333
-
334
334
  return cast(BaseRpcProxy, _AioXmlRpcProxyFromSession())
335
335
 
336
336
 
@@ -389,45 +389,23 @@ class SessionPlayer:
389
389
  """Return the secondary store for the given file_id."""
390
390
  return [fid for fid in self._store if fid != self._file_id]
391
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:
397
- """
398
- Load data from disk into the dictionary.
399
-
400
- Supports plain JSON files and ZIP archives containing a JSON file.
401
- When a ZIP archive is provided, the first JSON member inside the archive
402
- will be loaded.
403
- """
404
-
405
- if self.supports_file_id(file_id=file_id):
406
- return DataOperationResult.NO_LOAD
407
-
408
- if not os.path.exists(file_path):
409
- return DataOperationResult.NO_LOAD
410
-
411
- def _perform_load() -> DataOperationResult:
412
- try:
413
- if zipfile.is_zipfile(file_path):
414
- with zipfile.ZipFile(file_path, mode="r") as zf:
415
- # Prefer json files; pick the first .json entry if available
416
- if not (json_members := [n for n in zf.namelist() if n.lower().endswith(".json")]):
417
- return DataOperationResult.LOAD_FAIL
418
- raw = zf.read(json_members[0]).decode(UTF_8)
419
- data = json.loads(raw)
420
- else:
421
- with open(file=file_path, encoding=UTF_8) as file_pointer:
422
- data = json.loads(file_pointer.read())
423
-
424
- self._store[file_id] = data
425
- except (json.JSONDecodeError, zipfile.BadZipFile, UnicodeDecodeError, OSError):
426
- return DataOperationResult.LOAD_FAIL
427
- return DataOperationResult.LOAD_SUCCESS
392
+ def get_latest_response_by_method(self, *, rpc_type: str, method: str) -> list[tuple[Any, Any]]:
393
+ """Return latest non-expired responses for a given (rpc_type, method)."""
394
+ if pri_result := self.get_latest_response_by_method_for_file_id(
395
+ file_id=self._file_id,
396
+ rpc_type=rpc_type,
397
+ method=method,
398
+ ):
399
+ return pri_result
428
400
 
429
- loop = asyncio.get_running_loop()
430
- return await loop.run_in_executor(None, _perform_load)
401
+ for secondary_file_id in self._secondary_file_ids:
402
+ if sec_result := self.get_latest_response_by_method_for_file_id(
403
+ file_id=secondary_file_id,
404
+ rpc_type=rpc_type,
405
+ method=method,
406
+ ):
407
+ return sec_result
408
+ return pri_result
431
409
 
432
410
  def get_latest_response_by_method_for_file_id(
433
411
  self, *, file_id: str, rpc_type: str, method: str
@@ -453,20 +431,28 @@ class SessionPlayer:
453
431
  result.append((params, resp))
454
432
  return result
455
433
 
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(
434
+ def get_latest_response_by_params(
435
+ self,
436
+ *,
437
+ rpc_type: str,
438
+ method: str,
439
+ params: Any,
440
+ ) -> Any:
441
+ """Return latest non-expired responses for a given (rpc_type, method, params)."""
442
+ if pri_result := self.get_latest_response_by_params_for_file_id(
459
443
  file_id=self._file_id,
460
444
  rpc_type=rpc_type,
461
445
  method=method,
446
+ params=params,
462
447
  ):
463
448
  return pri_result
464
449
 
465
450
  for secondary_file_id in self._secondary_file_ids:
466
- if sec_result := self.get_latest_response_by_method_for_file_id(
451
+ if sec_result := self.get_latest_response_by_params_for_file_id(
467
452
  file_id=secondary_file_id,
468
453
  rpc_type=rpc_type,
469
454
  method=method,
455
+ params=params,
470
456
  ):
471
457
  return sec_result
472
458
  return pri_result
@@ -497,28 +483,42 @@ class SessionPlayer:
497
483
  except ValueError:
498
484
  return None
499
485
 
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
486
+ async def load(self, *, file_path: str, file_id: str) -> DataOperationResult:
487
+ """
488
+ Load data from disk into the dictionary.
515
489
 
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
490
+ Supports plain JSON files and ZIP archives containing a JSON file.
491
+ When a ZIP archive is provided, the first JSON member inside the archive
492
+ will be loaded.
493
+ """
494
+
495
+ if self.supports_file_id(file_id=file_id):
496
+ return DataOperationResult.NO_LOAD
497
+
498
+ if not os.path.exists(file_path):
499
+ return DataOperationResult.NO_LOAD
500
+
501
+ def _perform_load() -> DataOperationResult:
502
+ try:
503
+ if zipfile.is_zipfile(file_path):
504
+ with zipfile.ZipFile(file_path, mode="r") as zf:
505
+ # Prefer json files; pick the first .json entry if available
506
+ if not (json_members := [n for n in zf.namelist() if n.lower().endswith(".json")]):
507
+ return DataOperationResult.LOAD_FAIL
508
+ raw = zf.read(json_members[0]).decode(UTF_8)
509
+ data = json.loads(raw)
510
+ else:
511
+ with open(file=file_path, encoding=UTF_8) as file_pointer:
512
+ data = json.loads(file_pointer.read())
513
+
514
+ self._store[file_id] = data
515
+ except (json.JSONDecodeError, zipfile.BadZipFile, UnicodeDecodeError, OSError):
516
+ return DataOperationResult.LOAD_FAIL
517
+ return DataOperationResult.LOAD_SUCCESS
518
+
519
+ loop = asyncio.get_running_loop()
520
+ return await loop.run_in_executor(None, _perform_load)
521
+
522
+ def supports_file_id(self, *, file_id: str) -> bool:
523
+ """Return whether the session player supports the given file_id."""
524
+ return file_id in self._store
@@ -52,6 +52,62 @@ class FactoryWithClient:
52
52
  self.system_event_mock = MagicMock()
53
53
  self.ha_event_mock = MagicMock()
54
54
 
55
+ async def get_default_central(self, *, start: bool = True) -> CentralUnit:
56
+ """Return a central based on give address_device_translation."""
57
+ central = await self.get_raw_central()
58
+
59
+ await self._xml_proxy.do_init()
60
+ patch("aiohomematic.client.ClientConfig._create_xml_rpc_proxy", return_value=self._xml_proxy).start()
61
+ patch("aiohomematic.central.CentralUnit._identify_ip_addr", return_value=LOCAL_HOST).start()
62
+
63
+ # Optionally patch client creation to return a mocked client
64
+ if self._do_mock_client:
65
+ _orig_create_client = ClientConfig.create_client
66
+
67
+ async def _mocked_create_client(config: ClientConfig) -> Client | Mock:
68
+ real_client = await _orig_create_client(config)
69
+ return cast(
70
+ Mock,
71
+ get_mock(
72
+ instance=real_client,
73
+ exclude_methods=self._exclude_methods_from_mocks,
74
+ include_properties=self._include_properties_in_mocks,
75
+ ),
76
+ )
77
+
78
+ patch("aiohomematic.client.ClientConfig.create_client", _mocked_create_client).start()
79
+
80
+ if start:
81
+ await central.start()
82
+ await central._init_hub()
83
+ assert central
84
+ return central
85
+
86
+ async def get_raw_central(self) -> CentralUnit:
87
+ """Return a central based on give address_device_translation."""
88
+ interface_configs = self._interface_configs if self._interface_configs else set()
89
+ central = CentralConfig(
90
+ name=const.CENTRAL_NAME,
91
+ host=const.CCU_HOST,
92
+ username=const.CCU_USERNAME,
93
+ password=const.CCU_PASSWORD,
94
+ central_id="test1234",
95
+ interface_configs=interface_configs,
96
+ client_session=self._client_session,
97
+ un_ignore_list=self._un_ignore_list,
98
+ ignore_custom_device_definition_models=frozenset(self._ignore_custom_device_definition_models or []),
99
+ start_direct=True,
100
+ optional_settings=(OptionalSettings.ENABLE_LINKED_ENTITY_CLIMATE_ACTIVITY,),
101
+ ).create_central()
102
+
103
+ central.register_backend_system_callback(cb=self.system_event_mock)
104
+ central.register_homematic_callback(cb=self.ha_event_mock)
105
+
106
+ assert central
107
+ self._client_session.set_central(central=central) # type: ignore[attr-defined]
108
+ self._xml_proxy.set_central(central=central)
109
+ return central
110
+
55
111
  def init(
56
112
  self,
57
113
  *,
@@ -95,62 +151,6 @@ class FactoryWithClient:
95
151
  )
96
152
  return self
97
153
 
98
- async def get_raw_central(self) -> CentralUnit:
99
- """Return a central based on give address_device_translation."""
100
- interface_configs = self._interface_configs if self._interface_configs else set()
101
- central = CentralConfig(
102
- name=const.CENTRAL_NAME,
103
- host=const.CCU_HOST,
104
- username=const.CCU_USERNAME,
105
- password=const.CCU_PASSWORD,
106
- central_id="test1234",
107
- interface_configs=interface_configs,
108
- client_session=self._client_session,
109
- un_ignore_list=self._un_ignore_list,
110
- ignore_custom_device_definition_models=frozenset(self._ignore_custom_device_definition_models or []),
111
- start_direct=True,
112
- optional_settings=(OptionalSettings.ENABLE_LINKED_ENTITY_CLIMATE_ACTIVITY,),
113
- ).create_central()
114
-
115
- central.register_backend_system_callback(cb=self.system_event_mock)
116
- central.register_homematic_callback(cb=self.ha_event_mock)
117
-
118
- assert central
119
- self._client_session.set_central(central=central) # type: ignore[attr-defined]
120
- self._xml_proxy.set_central(central=central)
121
- return central
122
-
123
- async def get_default_central(self, *, start: bool = True) -> CentralUnit:
124
- """Return a central based on give address_device_translation."""
125
- central = await self.get_raw_central()
126
-
127
- await self._xml_proxy.do_init()
128
- patch("aiohomematic.client.ClientConfig._create_xml_rpc_proxy", return_value=self._xml_proxy).start()
129
- patch("aiohomematic.central.CentralUnit._identify_ip_addr", return_value=LOCAL_HOST).start()
130
-
131
- # Optionally patch client creation to return a mocked client
132
- if self._do_mock_client:
133
- _orig_create_client = ClientConfig.create_client
134
-
135
- async def _mocked_create_client(config: ClientConfig) -> Client | Mock:
136
- real_client = await _orig_create_client(config)
137
- return cast(
138
- Mock,
139
- get_mock(
140
- instance=real_client,
141
- exclude_methods=self._exclude_methods_from_mocks,
142
- include_properties=self._include_properties_in_mocks,
143
- ),
144
- )
145
-
146
- patch("aiohomematic.client.ClientConfig.create_client", _mocked_create_client).start()
147
-
148
- if start:
149
- await central.start()
150
- await central._init_hub()
151
- assert central
152
- return central
153
-
154
154
 
155
155
  async def get_central_client_factory(
156
156
  *,