aiohomematic-test-support 2025.12.13__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.
@@ -0,0 +1,275 @@
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
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import asyncio
35
+ from collections.abc import AsyncGenerator, Callable
36
+ import contextlib
37
+ import logging
38
+ from typing import Self, cast
39
+ from unittest.mock import MagicMock, Mock, patch
40
+
41
+ from aiohttp import ClientSession
42
+
43
+ from aiohomematic.central import CentralConfig, CentralUnit
44
+ from aiohomematic.central.event_bus import BackendSystemEventData, HomematicEvent
45
+ from aiohomematic.client import ClientConfig, InterfaceConfig
46
+ from aiohomematic.const import LOCAL_HOST, BackendSystemEvent, Interface, OptionalSettings
47
+ from aiohomematic.interfaces.client import ClientProtocol
48
+ from aiohomematic_test_support import const
49
+ from aiohomematic_test_support.mock import SessionPlayer, get_client_session, get_mock, get_xml_rpc_proxy
50
+
51
+ _LOGGER = logging.getLogger(__name__)
52
+
53
+
54
+ class FactoryWithClient:
55
+ """Factory for a central with one local client."""
56
+
57
+ def __init__(
58
+ self,
59
+ *,
60
+ player: SessionPlayer,
61
+ address_device_translation: set[str] | None = None,
62
+ do_mock_client: bool = True,
63
+ exclude_methods_from_mocks: set[str] | None = None,
64
+ ignore_custom_device_definition_models: list[str] | None = None,
65
+ ignore_devices_on_create: list[str] | None = None,
66
+ include_properties_in_mocks: set[str] | None = None,
67
+ interface_configs: set[InterfaceConfig] | None = None,
68
+ un_ignore_list: list[str] | None = None,
69
+ ) -> None:
70
+ """Init the central factory."""
71
+ self._player = player
72
+ self.init(
73
+ address_device_translation=address_device_translation,
74
+ do_mock_client=do_mock_client,
75
+ exclude_methods_from_mocks=exclude_methods_from_mocks,
76
+ ignore_custom_device_definition_models=ignore_custom_device_definition_models,
77
+ ignore_devices_on_create=ignore_devices_on_create,
78
+ include_properties_in_mocks=include_properties_in_mocks,
79
+ interface_configs=interface_configs,
80
+ un_ignore_list=un_ignore_list,
81
+ )
82
+ self.system_event_mock = MagicMock()
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()
91
+
92
+ async def get_default_central(self, *, start: bool = True) -> CentralUnit:
93
+ """Return a central based on give address_device_translation."""
94
+ central = await self.get_raw_central()
95
+
96
+ await self._xml_proxy.do_init()
97
+ patch("aiohomematic.client.ClientConfig._create_xml_rpc_proxy", return_value=self._xml_proxy).start()
98
+ patch("aiohomematic.central.CentralUnit._identify_ip_addr", return_value=LOCAL_HOST).start()
99
+
100
+ # Optionally patch client creation to return a mocked client
101
+ if self._do_mock_client:
102
+ _orig_create_client = ClientConfig.create_client
103
+
104
+ async def _mocked_create_client(config: ClientConfig) -> ClientProtocol | Mock:
105
+ real_client = await _orig_create_client(config)
106
+ return cast(
107
+ Mock,
108
+ get_mock(
109
+ instance=real_client,
110
+ exclude_methods=self._exclude_methods_from_mocks,
111
+ include_properties=self._include_properties_in_mocks,
112
+ ),
113
+ )
114
+
115
+ patch("aiohomematic.client.ClientConfig.create_client", _mocked_create_client).start()
116
+
117
+ if start:
118
+ await central.start()
119
+ await central.hub_coordinator.init_hub()
120
+ assert central
121
+ return central
122
+
123
+ async def get_raw_central(self) -> CentralUnit:
124
+ """Return a central based on give address_device_translation."""
125
+ interface_configs = self._interface_configs if self._interface_configs else set()
126
+ central = CentralConfig(
127
+ name=const.CENTRAL_NAME,
128
+ host=const.CCU_HOST,
129
+ username=const.CCU_USERNAME,
130
+ password=const.CCU_PASSWORD,
131
+ central_id="test1234",
132
+ interface_configs=interface_configs,
133
+ client_session=self._client_session,
134
+ un_ignore_list=self._un_ignore_list,
135
+ ignore_custom_device_definition_models=frozenset(self._ignore_custom_device_definition_models or []),
136
+ start_direct=True,
137
+ optional_settings=(OptionalSettings.ENABLE_LINKED_ENTITY_CLIMATE_ACTIVITY,),
138
+ ).create_central()
139
+
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
+ )
157
+
158
+ assert central
159
+ self._client_session.set_central(central=central) # type: ignore[attr-defined]
160
+ self._xml_proxy.set_central(central=central)
161
+ return central
162
+
163
+ def init(
164
+ self,
165
+ *,
166
+ address_device_translation: set[str] | None = None,
167
+ do_mock_client: bool = True,
168
+ exclude_methods_from_mocks: set[str] | None = None,
169
+ ignore_custom_device_definition_models: list[str] | None = None,
170
+ ignore_devices_on_create: list[str] | None = None,
171
+ include_properties_in_mocks: set[str] | None = None,
172
+ interface_configs: set[InterfaceConfig] | None = None,
173
+ un_ignore_list: list[str] | None = None,
174
+ ) -> Self:
175
+ """Init the central factory."""
176
+ self._address_device_translation = address_device_translation
177
+ self._do_mock_client = do_mock_client
178
+ self._exclude_methods_from_mocks = exclude_methods_from_mocks
179
+ self._ignore_custom_device_definition_models = ignore_custom_device_definition_models
180
+ self._ignore_devices_on_create = ignore_devices_on_create
181
+ self._include_properties_in_mocks = include_properties_in_mocks
182
+ self._interface_configs = (
183
+ interface_configs
184
+ if interface_configs is not None
185
+ else {
186
+ InterfaceConfig(
187
+ central_name=const.CENTRAL_NAME,
188
+ interface=Interface.BIDCOS_RF,
189
+ port=2001,
190
+ )
191
+ }
192
+ )
193
+ self._un_ignore_list = frozenset(un_ignore_list or [])
194
+ self._client_session = get_client_session(
195
+ player=self._player,
196
+ address_device_translation=self._address_device_translation,
197
+ ignore_devices_on_create=self._ignore_devices_on_create,
198
+ )
199
+ self._xml_proxy = get_xml_rpc_proxy(
200
+ player=self._player,
201
+ address_device_translation=self._address_device_translation,
202
+ ignore_devices_on_create=self._ignore_devices_on_create,
203
+ )
204
+ return self
205
+
206
+
207
+ async def get_central_client_factory(
208
+ *,
209
+ player: SessionPlayer,
210
+ address_device_translation: set[str],
211
+ do_mock_client: bool,
212
+ ignore_devices_on_create: list[str] | None,
213
+ ignore_custom_device_definition_models: list[str] | None,
214
+ un_ignore_list: list[str] | None,
215
+ ) -> AsyncGenerator[tuple[CentralUnit, ClientProtocol | Mock, FactoryWithClient]]:
216
+ """Return central factory."""
217
+ factory = FactoryWithClient(
218
+ player=player,
219
+ address_device_translation=address_device_translation,
220
+ do_mock_client=do_mock_client,
221
+ ignore_custom_device_definition_models=ignore_custom_device_definition_models,
222
+ ignore_devices_on_create=ignore_devices_on_create,
223
+ un_ignore_list=un_ignore_list,
224
+ )
225
+ central = await factory.get_default_central()
226
+ client = central.primary_client
227
+ assert client
228
+ try:
229
+ yield central, client, factory
230
+ finally:
231
+ factory.cleanup_event_bus_subscriptions()
232
+ await central.stop()
233
+ await central.clear_files()
234
+
235
+
236
+ async def get_pydev_ccu_central_unit_full(
237
+ *,
238
+ port: int,
239
+ client_session: ClientSession | None = None,
240
+ ) -> CentralUnit:
241
+ """Create and yield central, after all devices have been created."""
242
+ device_event = asyncio.Event()
243
+
244
+ def system_event_handler(event: BackendSystemEventData) -> None:
245
+ """Handle backend system events."""
246
+ if event.system_event == BackendSystemEvent.DEVICES_CREATED:
247
+ device_event.set()
248
+
249
+ interface_configs = {
250
+ InterfaceConfig(
251
+ central_name=const.CENTRAL_NAME,
252
+ interface=Interface.BIDCOS_RF,
253
+ port=port,
254
+ )
255
+ }
256
+
257
+ central = CentralConfig(
258
+ name=const.CENTRAL_NAME,
259
+ host=const.CCU_HOST,
260
+ username=const.CCU_USERNAME,
261
+ password=const.CCU_PASSWORD,
262
+ central_id="test1234",
263
+ interface_configs=interface_configs,
264
+ client_session=client_session,
265
+ program_markers=(),
266
+ sysvar_markers=(),
267
+ ).create_central()
268
+ central.event_bus.subscribe(event_type=BackendSystemEventData, event_key=None, handler=system_event_handler)
269
+ await central.start()
270
+
271
+ # Wait up to 60 seconds for the DEVICES_CREATED event which signals that all devices are available
272
+ with contextlib.suppress(TimeoutError):
273
+ await asyncio.wait_for(device_event.wait(), timeout=60)
274
+
275
+ return central
@@ -0,0 +1,47 @@
1
+ """Helpers for tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.resources
6
+ import logging
7
+ import os
8
+ from typing import Any
9
+
10
+ import orjson
11
+
12
+ from aiohomematic.central import CentralUnit
13
+ from aiohomematic.const import UTF_8
14
+ from aiohomematic.interfaces.model import CustomDataPointProtocol
15
+
16
+ _LOGGER = logging.getLogger(__name__)
17
+
18
+
19
+ # pylint: disable=protected-access
20
+
21
+
22
+ def _load_json_file(anchor: str, resource: str, file_name: str) -> Any | None:
23
+ """Load json file from disk into dict."""
24
+ package_path = str(importlib.resources.files(anchor))
25
+ with open(
26
+ file=os.path.join(package_path, resource, file_name),
27
+ encoding=UTF_8,
28
+ ) as fptr:
29
+ return orjson.loads(fptr.read())
30
+
31
+
32
+ def get_prepared_custom_data_point(
33
+ central: CentralUnit, address: str, channel_no: int
34
+ ) -> CustomDataPointProtocol | None:
35
+ """Return the hm custom_data_point."""
36
+ if cdp := central.get_custom_data_point(address=address, channel_no=channel_no):
37
+ for dp in cdp._data_points.values(): # type: ignore[attr-defined]
38
+ dp._state_uncertain = False
39
+ return cdp
40
+ return None
41
+
42
+
43
+ def load_device_description(file_name: str) -> Any:
44
+ """Load device description."""
45
+ dev_desc = _load_json_file(anchor="pydevccu", resource="device_descriptions", file_name=file_name)
46
+ assert dev_desc
47
+ return dev_desc