aiohomematic-test-support 2025.11.11__tar.gz → 2025.12.3__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 (18) hide show
  1. {aiohomematic_test_support-2025.11.11/aiohomematic_test_support.egg-info → aiohomematic_test_support-2025.12.3}/PKG-INFO +1 -1
  2. aiohomematic_test_support-2025.12.3/__init__.py +48 -0
  3. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3/aiohomematic_test_support.egg-info}/PKG-INFO +1 -1
  4. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/const.py +7 -0
  5. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/factory.py +67 -13
  6. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/helper.py +5 -3
  7. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/mock.py +43 -2
  8. aiohomematic_test_support-2025.11.11/__init__.py +0 -2
  9. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/MANIFEST.in +0 -0
  10. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/README.md +0 -0
  11. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/aiohomematic_test_support.egg-info/SOURCES.txt +0 -0
  12. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/aiohomematic_test_support.egg-info/dependency_links.txt +0 -0
  13. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/aiohomematic_test_support.egg-info/top_level.txt +0 -0
  14. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/data/full_session_randomized_ccu.zip +0 -0
  15. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/data/full_session_randomized_pydevccu.zip +0 -0
  16. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/py.typed +0 -0
  17. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/pyproject.toml +0 -0
  18. {aiohomematic_test_support-2025.11.11 → aiohomematic_test_support-2025.12.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic-test-support
3
- Version: 2025.11.11
3
+ Version: 2025.12.3
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,48 @@
1
+ """
2
+ Test support infrastructure for aiohomematic.
3
+
4
+ Overview
5
+ --------
6
+ This package provides reusable test utilities for testing aiohomematic functionality
7
+ without requiring a live Homematic backend. It includes factories for creating test
8
+ instances, mock implementations for RPC communication, and session playback capabilities
9
+ for reproducible testing.
10
+
11
+ Key Components
12
+ --------------
13
+ - **factory**: Factory classes for creating CentralUnit and Client instances with
14
+ pre-recorded or mocked backend responses.
15
+ - **mock**: Mock implementations for XML-RPC and JSON-RPC clients with session
16
+ playback support for deterministic testing.
17
+ - **helper**: Test helper utilities for common testing operations.
18
+ - **const**: Test-specific constants and configuration values.
19
+
20
+ Usage Example
21
+ -------------
22
+ Using the factory to create a test central with session playback:
23
+
24
+ from aiohomematic_test_support.factory import FactoryWithClient
25
+ from aiohomematic_test_support.mock import SessionPlayer
26
+
27
+ player = SessionPlayer(session_data_path="tests/data/ccu_session.zip")
28
+ factory = FactoryWithClient(player=player)
29
+ central, client = await factory()
30
+
31
+ # Test operations
32
+ await central.start()
33
+ device = central.get_device_by_address("VCU0000001")
34
+ await central.stop()
35
+
36
+ The session player replays pre-recorded backend responses, enabling fast and
37
+ reproducible tests without backend dependencies.
38
+
39
+ Notes
40
+ -----
41
+ This is a separate package from the main aiohomematic library. Install with
42
+ test dependencies to access test support functionality.
43
+
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ __version__ = "2025.12.3"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic-test-support
3
- Version: 2025.11.11
3
+ Version: 2025.12.3
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
@@ -13,6 +13,13 @@ CCU_PORT = 2002
13
13
  CCU_MINI_PORT = 2003
14
14
  INTERFACE_ID = f"{CENTRAL_NAME}-{Interface.BIDCOS_RF}"
15
15
 
16
+ # Backend info response for get_backend_info.fn script
17
+ BACKEND_INFO_JSON = {
18
+ "version": "3.75.6.20240316",
19
+ "product": "CCU3",
20
+ "hostname": "ccu-test",
21
+ }
22
+
16
23
  FULL_SESSION_RANDOMIZED_PYDEVCCU = "full_session_randomized_pydevccu.zip"
17
24
  FULL_SESSION_RANDOMIZED_CCU = "full_session_randomized_ccu.zip"
18
25
 
@@ -1,26 +1,56 @@
1
- """Factories for tests."""
1
+ """
2
+ Test factories for creating CentralUnit and Client instances.
3
+
4
+ This module provides factory classes that simplify the creation of test instances
5
+ with pre-configured mocks and session playback. Factories handle the complexity of
6
+ setting up CentralConfig, InterfaceConfig, and mock RPC clients.
7
+
8
+ Key Classes
9
+ -----------
10
+ - **FactoryWithClient**: Factory for creating a CentralUnit with a single mocked client.
11
+ - **CentralClientFactory**: Factory for creating central with multiple clients.
12
+
13
+ Usage Pattern
14
+ -------------
15
+ Factories use the builder pattern with fluent configuration:
16
+
17
+ factory = FactoryWithClient(
18
+ player=session_player,
19
+ do_mock_client=True,
20
+ ignore_devices_on_create=["VCU0000099"],
21
+ )
22
+ central, client = await factory()
23
+
24
+ # Use central and client for testing
25
+ await central.start()
26
+ # ... test operations ...
27
+ await central.stop()
28
+
29
+ Public API of this module is defined by __all__.
30
+ """
2
31
 
3
32
  from __future__ import annotations
4
33
 
5
34
  import asyncio
6
- from collections.abc import AsyncGenerator
35
+ from collections.abc import AsyncGenerator, Callable
7
36
  import contextlib
8
37
  import logging
9
- from typing import Any, Self, cast
38
+ from typing import Self, cast
10
39
  from unittest.mock import MagicMock, Mock, patch
11
40
 
12
41
  from aiohttp import ClientSession
13
42
 
14
43
  from aiohomematic.central import CentralConfig, CentralUnit
15
- from aiohomematic.client import Client, ClientConfig, InterfaceConfig
44
+ from aiohomematic.central.event_bus import BackendSystemEventData, HomematicEvent
45
+ from aiohomematic.client import ClientConfig, InterfaceConfig
16
46
  from aiohomematic.const import LOCAL_HOST, BackendSystemEvent, Interface, OptionalSettings
47
+ from aiohomematic.interfaces import ClientProtocol
17
48
  from aiohomematic_test_support import const
18
49
  from aiohomematic_test_support.mock import SessionPlayer, get_client_session, get_mock, get_xml_rpc_proxy
19
50
 
20
51
  _LOGGER = logging.getLogger(__name__)
21
52
 
22
53
 
23
- # pylint: disable=protected-access
24
54
  class FactoryWithClient:
25
55
  """Factory for a central with one local client."""
26
56
 
@@ -51,6 +81,13 @@ class FactoryWithClient:
51
81
  )
52
82
  self.system_event_mock = MagicMock()
53
83
  self.ha_event_mock = MagicMock()
84
+ self._event_bus_unsubscribe_handlers: list[Callable[[], None]] = []
85
+
86
+ def cleanup_event_bus_subscriptions(self) -> None:
87
+ """Clean up all event bus subscriptions."""
88
+ for unsubscribe in self._event_bus_unsubscribe_handlers:
89
+ unsubscribe()
90
+ self._event_bus_unsubscribe_handlers.clear()
54
91
 
55
92
  async def get_default_central(self, *, start: bool = True) -> CentralUnit:
56
93
  """Return a central based on give address_device_translation."""
@@ -64,7 +101,7 @@ class FactoryWithClient:
64
101
  if self._do_mock_client:
65
102
  _orig_create_client = ClientConfig.create_client
66
103
 
67
- async def _mocked_create_client(config: ClientConfig) -> Client | Mock:
104
+ async def _mocked_create_client(config: ClientConfig) -> ClientProtocol | Mock:
68
105
  real_client = await _orig_create_client(config)
69
106
  return cast(
70
107
  Mock,
@@ -79,7 +116,7 @@ class FactoryWithClient:
79
116
 
80
117
  if start:
81
118
  await central.start()
82
- await central._init_hub()
119
+ await central.hub_coordinator.init_hub()
83
120
  assert central
84
121
  return central
85
122
 
@@ -100,8 +137,23 @@ class FactoryWithClient:
100
137
  optional_settings=(OptionalSettings.ENABLE_LINKED_ENTITY_CLIMATE_ACTIVITY,),
101
138
  ).create_central()
102
139
 
103
- central.register_backend_system_callback(cb=self.system_event_mock)
104
- central.register_homematic_callback(cb=self.ha_event_mock)
140
+ # Subscribe to events via event bus
141
+ def _system_event_handler(event: BackendSystemEventData) -> None:
142
+ """Handle backend system events."""
143
+ self.system_event_mock(event)
144
+
145
+ def _ha_event_handler(event: HomematicEvent) -> None:
146
+ """Handle homematic events."""
147
+ self.ha_event_mock(event)
148
+
149
+ self._event_bus_unsubscribe_handlers.append(
150
+ central.event_bus.subscribe(
151
+ event_type=BackendSystemEventData, event_key=None, handler=_system_event_handler
152
+ )
153
+ )
154
+ self._event_bus_unsubscribe_handlers.append(
155
+ central.event_bus.subscribe(event_type=HomematicEvent, event_key=None, handler=_ha_event_handler)
156
+ )
105
157
 
106
158
  assert central
107
159
  self._client_session.set_central(central=central) # type: ignore[attr-defined]
@@ -160,7 +212,7 @@ async def get_central_client_factory(
160
212
  ignore_devices_on_create: list[str] | None,
161
213
  ignore_custom_device_definition_models: list[str] | None,
162
214
  un_ignore_list: list[str] | None,
163
- ) -> AsyncGenerator[tuple[CentralUnit, Client | Mock, FactoryWithClient]]:
215
+ ) -> AsyncGenerator[tuple[CentralUnit, ClientProtocol | Mock, FactoryWithClient]]:
164
216
  """Return central factory."""
165
217
  factory = FactoryWithClient(
166
218
  player=player,
@@ -176,6 +228,7 @@ async def get_central_client_factory(
176
228
  try:
177
229
  yield central, client, factory
178
230
  finally:
231
+ factory.cleanup_event_bus_subscriptions()
179
232
  await central.stop()
180
233
  await central.clear_files()
181
234
 
@@ -188,8 +241,9 @@ async def get_pydev_ccu_central_unit_full(
188
241
  """Create and yield central, after all devices have been created."""
189
242
  device_event = asyncio.Event()
190
243
 
191
- def systemcallback(system_event: Any, *args: Any, **kwargs: Any) -> None:
192
- if system_event == BackendSystemEvent.DEVICES_CREATED:
244
+ def system_event_handler(event: BackendSystemEventData) -> None:
245
+ """Handle backend system events."""
246
+ if event.system_event == BackendSystemEvent.DEVICES_CREATED:
193
247
  device_event.set()
194
248
 
195
249
  interface_configs = {
@@ -211,7 +265,7 @@ async def get_pydev_ccu_central_unit_full(
211
265
  program_markers=(),
212
266
  sysvar_markers=(),
213
267
  ).create_central()
214
- central.register_backend_system_callback(cb=systemcallback)
268
+ central.event_bus.subscribe(event_type=BackendSystemEventData, event_key=None, handler=system_event_handler)
215
269
  await central.start()
216
270
 
217
271
  # Wait up to 60 seconds for the DEVICES_CREATED event which signals that all devices are available
@@ -11,7 +11,7 @@ import orjson
11
11
 
12
12
  from aiohomematic.central import CentralUnit
13
13
  from aiohomematic.const import UTF_8
14
- from aiohomematic.model.custom import CustomDataPoint
14
+ from aiohomematic.interfaces import CustomDataPointProtocol
15
15
 
16
16
  _LOGGER = logging.getLogger(__name__)
17
17
 
@@ -29,10 +29,12 @@ def _load_json_file(anchor: str, resource: str, file_name: str) -> Any | None:
29
29
  return orjson.loads(fptr.read())
30
30
 
31
31
 
32
- def get_prepared_custom_data_point(central: CentralUnit, address: str, channel_no: int) -> CustomDataPoint | None:
32
+ def get_prepared_custom_data_point(
33
+ central: CentralUnit, address: str, channel_no: int
34
+ ) -> CustomDataPointProtocol | None:
33
35
  """Return the hm custom_data_point."""
34
36
  if cdp := central.get_custom_data_point(address=address, channel_no=channel_no):
35
- for dp in cdp._data_points.values():
37
+ for dp in cdp._data_points.values(): # type: ignore[attr-defined]
36
38
  dp._state_uncertain = False
37
39
  return cdp
38
40
  return None
@@ -1,4 +1,37 @@
1
- """Mocks for tests."""
1
+ """
2
+ Mock implementations for RPC clients with session playback.
3
+
4
+ This module provides mock RPC proxy implementations that replay pre-recorded
5
+ backend responses from session data files. This enables deterministic, fast
6
+ testing without live Homematic backend dependencies.
7
+
8
+ Key Classes
9
+ -----------
10
+ - **SessionPlayer**: Loads and plays back recorded RPC session data from ZIP archives.
11
+ - **get_mock**: Creates mock instances of data points and devices with configurable
12
+ method/property exclusions.
13
+ - **get_xml_rpc_proxy**: Returns mock XML-RPC proxy with session playback.
14
+ - **get_client_session**: Returns mock aiohttp ClientSession for JSON-RPC tests.
15
+
16
+ Session Playback
17
+ ----------------
18
+ Session data is stored in ZIP archives containing JSON files with recorded
19
+ RPC method calls and responses. The SessionPlayer replays these responses
20
+ when tests invoke RPC methods:
21
+
22
+ player = SessionPlayer(session_data_path="tests/data/ccu_full.zip")
23
+ proxy = get_xml_rpc_proxy(player=player, interface="BidCos-RF")
24
+
25
+ # Calls return pre-recorded responses
26
+ devices = await proxy.listDevices()
27
+
28
+ This approach provides:
29
+ - Fast test execution (no network I/O)
30
+ - Reproducible results (same responses every time)
31
+ - Offline testing (no backend required)
32
+
33
+ Public API of this module is defined by __all__.
34
+ """
2
35
 
3
36
  from __future__ import annotations
4
37
 
@@ -141,6 +174,15 @@ def get_client_session( # noqa: C901
141
174
  }
142
175
  )
143
176
 
177
+ if "get_backend_info" in params[_JsonKey.SCRIPT]:
178
+ return _MockResponse(
179
+ json_data={
180
+ _JsonKey.ID: 0,
181
+ _JsonKey.RESULT: const.BACKEND_INFO_JSON,
182
+ _JsonKey.ERROR: None,
183
+ }
184
+ )
185
+
144
186
  if method == _JsonRpcMethod.INTERFACE_SET_VALUE:
145
187
  await self._central.data_point_event(
146
188
  interface_id=params[_JsonKey.INTERFACE],
@@ -491,7 +533,6 @@ class SessionPlayer:
491
533
  When a ZIP archive is provided, the first JSON member inside the archive
492
534
  will be loaded.
493
535
  """
494
-
495
536
  if self.supports_file_id(file_id=file_id):
496
537
  return DataOperationResult.NO_LOAD
497
538
 
@@ -1,2 +0,0 @@
1
- __version__ = "2025.11.11"
2
- """Module to support aiohomematic testing with a local client."""