aiohomematic-test-support 2025.12.40__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 +48 -0
- aiohomematic_test_support/const.py +262 -0
- aiohomematic_test_support/data/device_translation.json +364 -0
- aiohomematic_test_support/data/full_session_randomized_ccu.zip +0 -0
- aiohomematic_test_support/data/full_session_randomized_pydevccu.zip +0 -0
- aiohomematic_test_support/factory.py +314 -0
- aiohomematic_test_support/helper.py +47 -0
- aiohomematic_test_support/mock.py +741 -0
- aiohomematic_test_support/py.typed +0 -0
- aiohomematic_test_support-2025.12.40.dist-info/METADATA +12 -0
- aiohomematic_test_support-2025.12.40.dist-info/RECORD +13 -0
- aiohomematic_test_support-2025.12.40.dist-info/WHEEL +5 -0
- aiohomematic_test_support-2025.12.40.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,314 @@
|
|
|
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 Any, 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.integration_events import (
|
|
45
|
+
DataPointsCreatedEvent,
|
|
46
|
+
DeviceLifecycleEvent,
|
|
47
|
+
DeviceLifecycleEventType,
|
|
48
|
+
DeviceTriggerEvent,
|
|
49
|
+
SystemStatusEvent,
|
|
50
|
+
)
|
|
51
|
+
from aiohomematic.client import ClientConfig, InterfaceConfig
|
|
52
|
+
from aiohomematic.const import LOCAL_HOST, Interface, OptionalSettings
|
|
53
|
+
from aiohomematic.interfaces.client import ClientProtocol
|
|
54
|
+
from aiohomematic_test_support import const
|
|
55
|
+
from aiohomematic_test_support.mock import SessionPlayer, get_client_session, get_mock, get_xml_rpc_proxy
|
|
56
|
+
|
|
57
|
+
_LOGGER = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class FactoryWithClient:
|
|
61
|
+
"""Factory for a central with one local client."""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
player: SessionPlayer,
|
|
67
|
+
address_device_translation: set[str] | None = None,
|
|
68
|
+
do_mock_client: bool = True,
|
|
69
|
+
exclude_methods_from_mocks: set[str] | None = None,
|
|
70
|
+
ignore_custom_device_definition_models: list[str] | None = None,
|
|
71
|
+
ignore_devices_on_create: list[str] | None = None,
|
|
72
|
+
include_properties_in_mocks: set[str] | None = None,
|
|
73
|
+
interface_configs: set[InterfaceConfig] | None = None,
|
|
74
|
+
un_ignore_list: list[str] | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Init the central factory."""
|
|
77
|
+
self._player = player
|
|
78
|
+
self.init(
|
|
79
|
+
address_device_translation=address_device_translation,
|
|
80
|
+
do_mock_client=do_mock_client,
|
|
81
|
+
exclude_methods_from_mocks=exclude_methods_from_mocks,
|
|
82
|
+
ignore_custom_device_definition_models=ignore_custom_device_definition_models,
|
|
83
|
+
ignore_devices_on_create=ignore_devices_on_create,
|
|
84
|
+
include_properties_in_mocks=include_properties_in_mocks,
|
|
85
|
+
interface_configs=interface_configs,
|
|
86
|
+
un_ignore_list=un_ignore_list,
|
|
87
|
+
)
|
|
88
|
+
self.system_event_mock = MagicMock()
|
|
89
|
+
self.ha_event_mock = MagicMock()
|
|
90
|
+
self._event_bus_unsubscribe_callbacks: list[Callable[[], None]] = []
|
|
91
|
+
self._patches: list[Any] = []
|
|
92
|
+
|
|
93
|
+
def cleanup(self) -> None:
|
|
94
|
+
"""Clean up all patches and event bus subscriptions."""
|
|
95
|
+
for unsubscribe in self._event_bus_unsubscribe_callbacks:
|
|
96
|
+
unsubscribe()
|
|
97
|
+
self._event_bus_unsubscribe_callbacks.clear()
|
|
98
|
+
for p in self._patches:
|
|
99
|
+
p.stop()
|
|
100
|
+
self._patches.clear()
|
|
101
|
+
|
|
102
|
+
def cleanup_event_bus_subscriptions(self) -> None:
|
|
103
|
+
"""Clean up all event bus subscriptions. Deprecated: use cleanup() instead."""
|
|
104
|
+
self.cleanup()
|
|
105
|
+
|
|
106
|
+
async def get_default_central(self, *, start: bool = True) -> CentralUnit:
|
|
107
|
+
"""Return a central based on give address_device_translation."""
|
|
108
|
+
central = await self.get_raw_central()
|
|
109
|
+
|
|
110
|
+
await self._xml_proxy.do_init()
|
|
111
|
+
p1 = patch("aiohomematic.client.ClientConfig._create_xml_rpc_proxy", return_value=self._xml_proxy)
|
|
112
|
+
p2 = patch("aiohomematic.central.CentralUnit._identify_ip_addr", return_value=LOCAL_HOST)
|
|
113
|
+
p1.start()
|
|
114
|
+
p2.start()
|
|
115
|
+
self._patches.extend([p1, p2])
|
|
116
|
+
|
|
117
|
+
# Optionally patch client creation to return a mocked client
|
|
118
|
+
if self._do_mock_client:
|
|
119
|
+
_orig_create_client = ClientConfig.create_client
|
|
120
|
+
|
|
121
|
+
async def _mocked_create_client(config: ClientConfig) -> ClientProtocol | Mock:
|
|
122
|
+
real_client = await _orig_create_client(config)
|
|
123
|
+
return cast(
|
|
124
|
+
Mock,
|
|
125
|
+
get_mock(
|
|
126
|
+
instance=real_client,
|
|
127
|
+
exclude_methods=self._exclude_methods_from_mocks,
|
|
128
|
+
include_properties=self._include_properties_in_mocks,
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
p3 = patch("aiohomematic.client.ClientConfig.create_client", _mocked_create_client)
|
|
133
|
+
p3.start()
|
|
134
|
+
self._patches.append(p3)
|
|
135
|
+
|
|
136
|
+
if start:
|
|
137
|
+
await central.start()
|
|
138
|
+
await central.hub_coordinator.init_hub()
|
|
139
|
+
assert central
|
|
140
|
+
return central
|
|
141
|
+
|
|
142
|
+
async def get_raw_central(self) -> CentralUnit:
|
|
143
|
+
"""Return a central based on give address_device_translation."""
|
|
144
|
+
interface_configs = self._interface_configs if self._interface_configs else set()
|
|
145
|
+
central = CentralConfig(
|
|
146
|
+
name=const.CENTRAL_NAME,
|
|
147
|
+
host=const.CCU_HOST,
|
|
148
|
+
username=const.CCU_USERNAME,
|
|
149
|
+
password=const.CCU_PASSWORD,
|
|
150
|
+
central_id="test1234",
|
|
151
|
+
interface_configs=interface_configs,
|
|
152
|
+
client_session=self._client_session,
|
|
153
|
+
un_ignore_list=self._un_ignore_list,
|
|
154
|
+
ignore_custom_device_definition_models=frozenset(self._ignore_custom_device_definition_models or []),
|
|
155
|
+
start_direct=True,
|
|
156
|
+
optional_settings=(OptionalSettings.ENABLE_LINKED_ENTITY_CLIMATE_ACTIVITY,),
|
|
157
|
+
).create_central()
|
|
158
|
+
|
|
159
|
+
# Subscribe to events via event bus
|
|
160
|
+
def _device_lifecycle_event_handler(event: DeviceLifecycleEvent) -> None:
|
|
161
|
+
"""Handle device lifecycle events."""
|
|
162
|
+
self.system_event_mock(event)
|
|
163
|
+
|
|
164
|
+
def _data_points_created_event_handler(event: DataPointsCreatedEvent) -> None:
|
|
165
|
+
"""Handle data points created events."""
|
|
166
|
+
self.system_event_mock(event)
|
|
167
|
+
|
|
168
|
+
def _device_trigger_event_handler(event: DeviceTriggerEvent) -> None:
|
|
169
|
+
"""Handle device trigger events."""
|
|
170
|
+
self.ha_event_mock(event)
|
|
171
|
+
|
|
172
|
+
def _system_status_event_handler(event: SystemStatusEvent) -> None:
|
|
173
|
+
"""Handle system status events (issues, state changes)."""
|
|
174
|
+
self.ha_event_mock(event)
|
|
175
|
+
|
|
176
|
+
self._event_bus_unsubscribe_callbacks.append(
|
|
177
|
+
central.event_bus.subscribe(
|
|
178
|
+
event_type=DeviceLifecycleEvent, event_key=None, handler=_device_lifecycle_event_handler
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
self._event_bus_unsubscribe_callbacks.append(
|
|
182
|
+
central.event_bus.subscribe(
|
|
183
|
+
event_type=DataPointsCreatedEvent, event_key=None, handler=_data_points_created_event_handler
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
self._event_bus_unsubscribe_callbacks.append(
|
|
187
|
+
central.event_bus.subscribe(
|
|
188
|
+
event_type=DeviceTriggerEvent, event_key=None, handler=_device_trigger_event_handler
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
self._event_bus_unsubscribe_callbacks.append(
|
|
192
|
+
central.event_bus.subscribe(
|
|
193
|
+
event_type=SystemStatusEvent, event_key=None, handler=_system_status_event_handler
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
assert central
|
|
198
|
+
self._client_session.set_central(central=central) # type: ignore[attr-defined]
|
|
199
|
+
self._xml_proxy.set_central(central=central)
|
|
200
|
+
return central
|
|
201
|
+
|
|
202
|
+
def init(
|
|
203
|
+
self,
|
|
204
|
+
*,
|
|
205
|
+
address_device_translation: set[str] | None = None,
|
|
206
|
+
do_mock_client: bool = True,
|
|
207
|
+
exclude_methods_from_mocks: set[str] | None = None,
|
|
208
|
+
ignore_custom_device_definition_models: list[str] | None = None,
|
|
209
|
+
ignore_devices_on_create: list[str] | None = None,
|
|
210
|
+
include_properties_in_mocks: set[str] | None = None,
|
|
211
|
+
interface_configs: set[InterfaceConfig] | None = None,
|
|
212
|
+
un_ignore_list: list[str] | None = None,
|
|
213
|
+
) -> Self:
|
|
214
|
+
"""Init the central factory."""
|
|
215
|
+
self._address_device_translation = address_device_translation
|
|
216
|
+
self._do_mock_client = do_mock_client
|
|
217
|
+
self._exclude_methods_from_mocks = exclude_methods_from_mocks
|
|
218
|
+
self._ignore_custom_device_definition_models = ignore_custom_device_definition_models
|
|
219
|
+
self._ignore_devices_on_create = ignore_devices_on_create
|
|
220
|
+
self._include_properties_in_mocks = include_properties_in_mocks
|
|
221
|
+
self._interface_configs = (
|
|
222
|
+
interface_configs
|
|
223
|
+
if interface_configs is not None
|
|
224
|
+
else {
|
|
225
|
+
InterfaceConfig(
|
|
226
|
+
central_name=const.CENTRAL_NAME,
|
|
227
|
+
interface=Interface.BIDCOS_RF,
|
|
228
|
+
port=2001,
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
self._un_ignore_list = frozenset(un_ignore_list or [])
|
|
233
|
+
self._client_session = get_client_session(
|
|
234
|
+
player=self._player,
|
|
235
|
+
address_device_translation=self._address_device_translation,
|
|
236
|
+
ignore_devices_on_create=self._ignore_devices_on_create,
|
|
237
|
+
)
|
|
238
|
+
self._xml_proxy = get_xml_rpc_proxy(
|
|
239
|
+
player=self._player,
|
|
240
|
+
address_device_translation=self._address_device_translation,
|
|
241
|
+
ignore_devices_on_create=self._ignore_devices_on_create,
|
|
242
|
+
)
|
|
243
|
+
return self
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
async def get_central_client_factory(
|
|
247
|
+
*,
|
|
248
|
+
player: SessionPlayer,
|
|
249
|
+
address_device_translation: set[str],
|
|
250
|
+
do_mock_client: bool,
|
|
251
|
+
ignore_devices_on_create: list[str] | None,
|
|
252
|
+
ignore_custom_device_definition_models: list[str] | None,
|
|
253
|
+
un_ignore_list: list[str] | None,
|
|
254
|
+
) -> AsyncGenerator[tuple[CentralUnit, ClientProtocol | Mock, FactoryWithClient]]:
|
|
255
|
+
"""Return central factory."""
|
|
256
|
+
factory = FactoryWithClient(
|
|
257
|
+
player=player,
|
|
258
|
+
address_device_translation=address_device_translation,
|
|
259
|
+
do_mock_client=do_mock_client,
|
|
260
|
+
ignore_custom_device_definition_models=ignore_custom_device_definition_models,
|
|
261
|
+
ignore_devices_on_create=ignore_devices_on_create,
|
|
262
|
+
un_ignore_list=un_ignore_list,
|
|
263
|
+
)
|
|
264
|
+
central = await factory.get_default_central()
|
|
265
|
+
client = central.client_coordinator.primary_client
|
|
266
|
+
assert client
|
|
267
|
+
try:
|
|
268
|
+
yield central, client, factory
|
|
269
|
+
finally:
|
|
270
|
+
factory.cleanup()
|
|
271
|
+
await central.stop()
|
|
272
|
+
await central.cache_coordinator.clear_all()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
async def get_pydev_ccu_central_unit_full(
|
|
276
|
+
*,
|
|
277
|
+
port: int,
|
|
278
|
+
client_session: ClientSession | None = None,
|
|
279
|
+
) -> CentralUnit:
|
|
280
|
+
"""Create and yield central, after all devices have been created."""
|
|
281
|
+
device_event = asyncio.Event()
|
|
282
|
+
|
|
283
|
+
def device_lifecycle_event_handler(event: DeviceLifecycleEvent) -> None:
|
|
284
|
+
"""Handle device lifecycle events."""
|
|
285
|
+
if event.event_type == DeviceLifecycleEventType.CREATED:
|
|
286
|
+
device_event.set()
|
|
287
|
+
|
|
288
|
+
interface_configs = {
|
|
289
|
+
InterfaceConfig(
|
|
290
|
+
central_name=const.CENTRAL_NAME,
|
|
291
|
+
interface=Interface.BIDCOS_RF,
|
|
292
|
+
port=port,
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
central = CentralConfig(
|
|
297
|
+
name=const.CENTRAL_NAME,
|
|
298
|
+
host=const.CCU_HOST,
|
|
299
|
+
username=const.CCU_USERNAME,
|
|
300
|
+
password=const.CCU_PASSWORD,
|
|
301
|
+
central_id="test1234",
|
|
302
|
+
interface_configs=interface_configs,
|
|
303
|
+
client_session=client_session,
|
|
304
|
+
program_markers=(),
|
|
305
|
+
sysvar_markers=(),
|
|
306
|
+
).create_central()
|
|
307
|
+
central.event_bus.subscribe(event_type=DeviceLifecycleEvent, event_key=None, handler=device_lifecycle_event_handler)
|
|
308
|
+
await central.start()
|
|
309
|
+
|
|
310
|
+
# Wait up to 60 seconds for the DEVICES_CREATED event which signals that all devices are available
|
|
311
|
+
with contextlib.suppress(TimeoutError):
|
|
312
|
+
await asyncio.wait_for(device_event.wait(), timeout=60)
|
|
313
|
+
|
|
314
|
+
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
|