aiohomematic-test-support 2025.12.23__py3-none-any.whl → 2026.1.23__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.
- aiohomematic_test_support/__init__.py +33 -2
- aiohomematic_test_support/const.py +26 -365
- aiohomematic_test_support/data/device_translation.json +364 -0
- aiohomematic_test_support/event_capture.py +257 -0
- aiohomematic_test_support/event_mock.py +300 -0
- aiohomematic_test_support/factory.py +77 -29
- aiohomematic_test_support/helper.py +6 -4
- aiohomematic_test_support/mock.py +68 -23
- {aiohomematic_test_support-2025.12.23.dist-info → aiohomematic_test_support-2026.1.23.dist-info}/METADATA +2 -2
- aiohomematic_test_support-2026.1.23.dist-info/RECORD +15 -0
- aiohomematic_test_support-2025.12.23.dist-info/RECORD +0 -12
- {aiohomematic_test_support-2025.12.23.dist-info → aiohomematic_test_support-2026.1.23.dist-info}/WHEEL +0 -0
- {aiohomematic_test_support-2025.12.23.dist-info → aiohomematic_test_support-2026.1.23.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Event-driven mock server for test triggers.
|
|
5
|
+
|
|
6
|
+
Overview
|
|
7
|
+
--------
|
|
8
|
+
This module provides a mock server that responds to events by injecting
|
|
9
|
+
data or triggering actions. This enables event-based test orchestration
|
|
10
|
+
where test behavior is triggered by events rather than explicit calls.
|
|
11
|
+
|
|
12
|
+
The EventDrivenMockServer allows tests to:
|
|
13
|
+
- Configure responses to specific event types
|
|
14
|
+
- Inject mock data when certain events are published
|
|
15
|
+
- Build complex test scenarios with event-driven behavior
|
|
16
|
+
|
|
17
|
+
Public API
|
|
18
|
+
----------
|
|
19
|
+
- EventDrivenMockServer: Main mock server class
|
|
20
|
+
- ResponseBuilder: Fluent builder for configuring responses
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import TYPE_CHECKING, Any
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from aiohomematic.central.events import Event, EventBus
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class MockAction:
|
|
35
|
+
"""
|
|
36
|
+
An action to perform when an event is received.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
----------
|
|
40
|
+
handler: The callback to invoke when the event is received
|
|
41
|
+
filter_fn: Optional filter function to match specific events
|
|
42
|
+
one_shot: If True, the action is removed after first invocation
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
handler: Callable[[Any], Any]
|
|
47
|
+
filter_fn: Callable[[Any], bool] | None = None
|
|
48
|
+
one_shot: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class EventDrivenMockServer:
|
|
53
|
+
"""
|
|
54
|
+
Mock server that responds to events.
|
|
55
|
+
|
|
56
|
+
Provides a fluent API for configuring responses to specific event types.
|
|
57
|
+
When an event is published that matches a configured response, the
|
|
58
|
+
corresponding action is executed.
|
|
59
|
+
|
|
60
|
+
Example Usage
|
|
61
|
+
-------------
|
|
62
|
+
mock_server = EventDrivenMockServer(event_bus=central.event_bus)
|
|
63
|
+
|
|
64
|
+
# Configure response to inject data when refresh is triggered
|
|
65
|
+
mock_server.when(DataRefreshTriggeredEvent).then_call(
|
|
66
|
+
lambda event: inject_mock_data()
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Configure one-shot response
|
|
70
|
+
mock_server.when(CircuitBreakerTrippedEvent).once().then_call(
|
|
71
|
+
lambda event: trigger_recovery()
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Later, cleanup:
|
|
75
|
+
mock_server.cleanup()
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
_event_bus: EventBus = field(repr=False)
|
|
80
|
+
"""The EventBus to subscribe to."""
|
|
81
|
+
|
|
82
|
+
_responses: dict[type[Event], list[MockAction]] = field(default_factory=dict)
|
|
83
|
+
"""Mapping of event types to their configured actions."""
|
|
84
|
+
|
|
85
|
+
_unsubscribers: list[Callable[[], None]] = field(default_factory=list)
|
|
86
|
+
"""Unsubscribe callbacks for cleanup."""
|
|
87
|
+
|
|
88
|
+
_invocation_count: dict[type[Event], int] = field(default_factory=dict)
|
|
89
|
+
"""Count of invocations per event type."""
|
|
90
|
+
|
|
91
|
+
def cleanup(self) -> None:
|
|
92
|
+
"""Unsubscribe from all events and clear responses."""
|
|
93
|
+
for unsub in self._unsubscribers:
|
|
94
|
+
unsub()
|
|
95
|
+
self._unsubscribers.clear()
|
|
96
|
+
self._responses.clear()
|
|
97
|
+
self._invocation_count.clear()
|
|
98
|
+
|
|
99
|
+
def get_invocation_count(self, *, event_type: type[Event]) -> int:
|
|
100
|
+
"""
|
|
101
|
+
Return the number of times handlers were invoked for an event type.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
----
|
|
105
|
+
event_type: The event type to query
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
-------
|
|
109
|
+
Number of handler invocations
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
return self._invocation_count.get(event_type, 0)
|
|
113
|
+
|
|
114
|
+
def when[T: Event](self, *, event_type: type[T]) -> ResponseBuilder[T]:
|
|
115
|
+
"""
|
|
116
|
+
Start configuring a response for an event type.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
----
|
|
120
|
+
event_type: The event type to respond to
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
-------
|
|
124
|
+
A ResponseBuilder for configuring the response
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
return ResponseBuilder(mock_server=self, event_type=event_type)
|
|
128
|
+
|
|
129
|
+
def _add_response(
|
|
130
|
+
self,
|
|
131
|
+
*,
|
|
132
|
+
event_type: type[Event],
|
|
133
|
+
action: MockAction,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Add a response for an event type.
|
|
137
|
+
|
|
138
|
+
If this is the first response for this event type, subscribe to it.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
----
|
|
142
|
+
event_type: The event type to respond to
|
|
143
|
+
action: The action to perform
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
if event_type not in self._responses:
|
|
147
|
+
self._responses[event_type] = []
|
|
148
|
+
# Subscribe to this event type
|
|
149
|
+
unsub = self._event_bus.subscribe(
|
|
150
|
+
event_type=event_type,
|
|
151
|
+
event_key=None,
|
|
152
|
+
handler=lambda event, et=event_type: self._on_event(event=event, event_type=et),
|
|
153
|
+
)
|
|
154
|
+
self._unsubscribers.append(unsub)
|
|
155
|
+
|
|
156
|
+
self._responses[event_type].append(action)
|
|
157
|
+
|
|
158
|
+
def _on_event(self, *, event: Event, event_type: type[Event]) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Handle an event by invoking matching responses.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
----
|
|
164
|
+
event: The event that was published
|
|
165
|
+
event_type: The event type (for lookup)
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
if event_type not in self._responses:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
# Track invocations
|
|
172
|
+
self._invocation_count[event_type] = self._invocation_count.get(event_type, 0) + 1
|
|
173
|
+
|
|
174
|
+
# Process actions (copy list to allow modification during iteration)
|
|
175
|
+
actions_to_remove: list[MockAction] = []
|
|
176
|
+
for action in list(self._responses[event_type]):
|
|
177
|
+
# Check filter if present
|
|
178
|
+
if action.filter_fn is not None and not action.filter_fn(event):
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Invoke handler
|
|
182
|
+
action.handler(event)
|
|
183
|
+
|
|
184
|
+
# Mark one-shot actions for removal
|
|
185
|
+
if action.one_shot:
|
|
186
|
+
actions_to_remove.append(action)
|
|
187
|
+
|
|
188
|
+
# Remove one-shot actions
|
|
189
|
+
for action in actions_to_remove:
|
|
190
|
+
self._responses[event_type].remove(action)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class ResponseBuilder[T: "Event"]:
|
|
195
|
+
"""
|
|
196
|
+
Fluent builder for configuring mock responses.
|
|
197
|
+
|
|
198
|
+
Provides a chainable API for configuring how the mock server should
|
|
199
|
+
respond to specific events.
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
-------
|
|
203
|
+
mock_server.when(SomeEvent)
|
|
204
|
+
.matching(lambda e: e.interface_id == "test")
|
|
205
|
+
.once()
|
|
206
|
+
.then_call(handler_fn)
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
mock_server: EventDrivenMockServer
|
|
211
|
+
"""The mock server to configure."""
|
|
212
|
+
|
|
213
|
+
event_type: type[T]
|
|
214
|
+
"""The event type being configured."""
|
|
215
|
+
|
|
216
|
+
_filter_fn: Callable[[T], bool] | None = None
|
|
217
|
+
"""Optional filter function."""
|
|
218
|
+
|
|
219
|
+
_one_shot: bool = False
|
|
220
|
+
"""Whether this is a one-shot response."""
|
|
221
|
+
|
|
222
|
+
def matching(self, *, filter_fn: Callable[[T], bool]) -> ResponseBuilder[T]:
|
|
223
|
+
"""
|
|
224
|
+
Add a filter to match specific events.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
----
|
|
228
|
+
filter_fn: Function that returns True for matching events
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
-------
|
|
232
|
+
Self for chaining
|
|
233
|
+
|
|
234
|
+
"""
|
|
235
|
+
self._filter_fn = filter_fn
|
|
236
|
+
return self
|
|
237
|
+
|
|
238
|
+
def once(self) -> ResponseBuilder[T]:
|
|
239
|
+
"""
|
|
240
|
+
Make this a one-shot response (removed after first invocation).
|
|
241
|
+
|
|
242
|
+
Returns
|
|
243
|
+
-------
|
|
244
|
+
Self for chaining
|
|
245
|
+
|
|
246
|
+
"""
|
|
247
|
+
self._one_shot = True
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
def then_call(self, *, handler: Callable[[T], Any]) -> None:
|
|
251
|
+
"""
|
|
252
|
+
Configure the handler to call when the event is received.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
----
|
|
256
|
+
handler: Function to call with the event
|
|
257
|
+
|
|
258
|
+
"""
|
|
259
|
+
self.mock_server._add_response( # noqa: SLF001 # pylint: disable=protected-access
|
|
260
|
+
event_type=self.event_type,
|
|
261
|
+
action=MockAction(
|
|
262
|
+
handler=handler,
|
|
263
|
+
filter_fn=self._filter_fn,
|
|
264
|
+
one_shot=self._one_shot,
|
|
265
|
+
),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def then_publish(self, *, event_factory: Callable[[T], Event]) -> None:
|
|
269
|
+
"""
|
|
270
|
+
Configure the mock to publish another event when this event is received.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
----
|
|
274
|
+
event_factory: Function that creates the event to publish
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
def publish_handler(event: T) -> None:
|
|
279
|
+
new_event = event_factory(event)
|
|
280
|
+
self.mock_server._event_bus.publish_sync(event=new_event) # noqa: SLF001 # pylint: disable=protected-access
|
|
281
|
+
|
|
282
|
+
self.then_call(handler=publish_handler)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def create_event_mock_server(*, event_bus: EventBus) -> EventDrivenMockServer:
|
|
286
|
+
"""
|
|
287
|
+
Create an EventDrivenMockServer instance.
|
|
288
|
+
|
|
289
|
+
Factory function for creating an event-driven mock server.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
----
|
|
293
|
+
event_bus: The EventBus to subscribe to
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
-------
|
|
297
|
+
Configured EventDrivenMockServer instance
|
|
298
|
+
|
|
299
|
+
"""
|
|
300
|
+
return EventDrivenMockServer(_event_bus=event_bus)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
1
3
|
"""
|
|
2
4
|
Test factories for creating CentralUnit and Client instances.
|
|
3
5
|
|
|
@@ -35,16 +37,22 @@ import asyncio
|
|
|
35
37
|
from collections.abc import AsyncGenerator, Callable
|
|
36
38
|
import contextlib
|
|
37
39
|
import logging
|
|
38
|
-
from typing import Self, cast
|
|
40
|
+
from typing import Any, Self, cast
|
|
39
41
|
from unittest.mock import MagicMock, Mock, patch
|
|
40
42
|
|
|
41
43
|
from aiohttp import ClientSession
|
|
42
44
|
|
|
43
45
|
from aiohomematic.central import CentralConfig, CentralUnit
|
|
44
|
-
from aiohomematic.central.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
from aiohomematic.central.events import (
|
|
47
|
+
DataPointsCreatedEvent,
|
|
48
|
+
DeviceLifecycleEvent,
|
|
49
|
+
DeviceLifecycleEventType,
|
|
50
|
+
DeviceTriggerEvent,
|
|
51
|
+
SystemStatusChangedEvent,
|
|
52
|
+
)
|
|
53
|
+
from aiohomematic.client import InterfaceConfig, create_client as create_client_func
|
|
54
|
+
from aiohomematic.const import LOCAL_HOST, Interface, OptionalSettings
|
|
55
|
+
from aiohomematic.interfaces import ClientProtocol
|
|
48
56
|
from aiohomematic_test_support import const
|
|
49
57
|
from aiohomematic_test_support.mock import SessionPlayer, get_client_session, get_mock, get_xml_rpc_proxy
|
|
50
58
|
|
|
@@ -82,27 +90,45 @@ class FactoryWithClient:
|
|
|
82
90
|
self.system_event_mock = MagicMock()
|
|
83
91
|
self.ha_event_mock = MagicMock()
|
|
84
92
|
self._event_bus_unsubscribe_callbacks: list[Callable[[], None]] = []
|
|
93
|
+
self._patches: list[Any] = []
|
|
85
94
|
|
|
86
|
-
def
|
|
87
|
-
"""Clean up all event bus subscriptions."""
|
|
95
|
+
def cleanup(self) -> None:
|
|
96
|
+
"""Clean up all patches and event bus subscriptions."""
|
|
88
97
|
for unsubscribe in self._event_bus_unsubscribe_callbacks:
|
|
89
98
|
unsubscribe()
|
|
90
99
|
self._event_bus_unsubscribe_callbacks.clear()
|
|
100
|
+
for p in self._patches:
|
|
101
|
+
p.stop()
|
|
102
|
+
self._patches.clear()
|
|
103
|
+
|
|
104
|
+
def cleanup_event_bus_subscriptions(self) -> None:
|
|
105
|
+
"""Clean up all event bus subscriptions. Deprecated: use cleanup() instead."""
|
|
106
|
+
self.cleanup()
|
|
91
107
|
|
|
92
108
|
async def get_default_central(self, *, start: bool = True) -> CentralUnit:
|
|
93
109
|
"""Return a central based on give address_device_translation."""
|
|
94
110
|
central = await self.get_raw_central()
|
|
95
111
|
|
|
96
112
|
await self._xml_proxy.do_init()
|
|
97
|
-
patch("aiohomematic.client.ClientConfig._create_xml_rpc_proxy", return_value=self._xml_proxy)
|
|
98
|
-
patch("aiohomematic.central.CentralUnit._identify_ip_addr", return_value=LOCAL_HOST)
|
|
113
|
+
p1 = patch("aiohomematic.client.ClientConfig._create_xml_rpc_proxy", return_value=self._xml_proxy)
|
|
114
|
+
p2 = patch("aiohomematic.central.CentralUnit._identify_ip_addr", return_value=LOCAL_HOST)
|
|
115
|
+
p1.start()
|
|
116
|
+
p2.start()
|
|
117
|
+
self._patches.extend([p1, p2])
|
|
99
118
|
|
|
100
119
|
# Optionally patch client creation to return a mocked client
|
|
101
120
|
if self._do_mock_client:
|
|
102
|
-
_orig_create_client =
|
|
103
|
-
|
|
104
|
-
async def _mocked_create_client(
|
|
105
|
-
|
|
121
|
+
_orig_create_client = create_client_func
|
|
122
|
+
|
|
123
|
+
async def _mocked_create_client(
|
|
124
|
+
*,
|
|
125
|
+
client_deps: Any,
|
|
126
|
+
interface_config: InterfaceConfig,
|
|
127
|
+
) -> ClientProtocol | Mock:
|
|
128
|
+
real_client = await _orig_create_client(
|
|
129
|
+
client_deps=client_deps,
|
|
130
|
+
interface_config=interface_config,
|
|
131
|
+
)
|
|
106
132
|
return cast(
|
|
107
133
|
Mock,
|
|
108
134
|
get_mock(
|
|
@@ -112,7 +138,9 @@ class FactoryWithClient:
|
|
|
112
138
|
),
|
|
113
139
|
)
|
|
114
140
|
|
|
115
|
-
patch("aiohomematic.client.
|
|
141
|
+
p3 = patch("aiohomematic.client.create_client", _mocked_create_client)
|
|
142
|
+
p3.start()
|
|
143
|
+
self._patches.append(p3)
|
|
116
144
|
|
|
117
145
|
if start:
|
|
118
146
|
await central.start()
|
|
@@ -123,7 +151,7 @@ class FactoryWithClient:
|
|
|
123
151
|
async def get_raw_central(self) -> CentralUnit:
|
|
124
152
|
"""Return a central based on give address_device_translation."""
|
|
125
153
|
interface_configs = self._interface_configs if self._interface_configs else set()
|
|
126
|
-
central = CentralConfig(
|
|
154
|
+
central = await CentralConfig(
|
|
127
155
|
name=const.CENTRAL_NAME,
|
|
128
156
|
host=const.CCU_HOST,
|
|
129
157
|
username=const.CCU_USERNAME,
|
|
@@ -138,21 +166,41 @@ class FactoryWithClient:
|
|
|
138
166
|
).create_central()
|
|
139
167
|
|
|
140
168
|
# Subscribe to events via event bus
|
|
141
|
-
def
|
|
142
|
-
"""Handle
|
|
169
|
+
def _device_lifecycle_event_handler(event: DeviceLifecycleEvent) -> None:
|
|
170
|
+
"""Handle device lifecycle events."""
|
|
143
171
|
self.system_event_mock(event)
|
|
144
172
|
|
|
145
|
-
def
|
|
146
|
-
"""Handle
|
|
173
|
+
def _data_points_created_event_handler(event: DataPointsCreatedEvent) -> None:
|
|
174
|
+
"""Handle data points created events."""
|
|
175
|
+
self.system_event_mock(event)
|
|
176
|
+
|
|
177
|
+
def _device_trigger_event_handler(event: DeviceTriggerEvent) -> None:
|
|
178
|
+
"""Handle device trigger events."""
|
|
179
|
+
self.ha_event_mock(event)
|
|
180
|
+
|
|
181
|
+
def _system_status_event_handler(event: SystemStatusChangedEvent) -> None:
|
|
182
|
+
"""Handle system status events (issues, state changes)."""
|
|
147
183
|
self.ha_event_mock(event)
|
|
148
184
|
|
|
149
185
|
self._event_bus_unsubscribe_callbacks.append(
|
|
150
186
|
central.event_bus.subscribe(
|
|
151
|
-
event_type=
|
|
187
|
+
event_type=DeviceLifecycleEvent, event_key=None, handler=_device_lifecycle_event_handler
|
|
152
188
|
)
|
|
153
189
|
)
|
|
154
190
|
self._event_bus_unsubscribe_callbacks.append(
|
|
155
|
-
central.event_bus.subscribe(
|
|
191
|
+
central.event_bus.subscribe(
|
|
192
|
+
event_type=DataPointsCreatedEvent, event_key=None, handler=_data_points_created_event_handler
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
self._event_bus_unsubscribe_callbacks.append(
|
|
196
|
+
central.event_bus.subscribe(
|
|
197
|
+
event_type=DeviceTriggerEvent, event_key=None, handler=_device_trigger_event_handler
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
self._event_bus_unsubscribe_callbacks.append(
|
|
201
|
+
central.event_bus.subscribe(
|
|
202
|
+
event_type=SystemStatusChangedEvent, event_key=None, handler=_system_status_event_handler
|
|
203
|
+
)
|
|
156
204
|
)
|
|
157
205
|
|
|
158
206
|
assert central
|
|
@@ -223,14 +271,14 @@ async def get_central_client_factory(
|
|
|
223
271
|
un_ignore_list=un_ignore_list,
|
|
224
272
|
)
|
|
225
273
|
central = await factory.get_default_central()
|
|
226
|
-
client = central.primary_client
|
|
274
|
+
client = central.client_coordinator.primary_client
|
|
227
275
|
assert client
|
|
228
276
|
try:
|
|
229
277
|
yield central, client, factory
|
|
230
278
|
finally:
|
|
231
|
-
factory.
|
|
279
|
+
factory.cleanup()
|
|
232
280
|
await central.stop()
|
|
233
|
-
await central.
|
|
281
|
+
await central.cache_coordinator.clear_all()
|
|
234
282
|
|
|
235
283
|
|
|
236
284
|
async def get_pydev_ccu_central_unit_full(
|
|
@@ -241,9 +289,9 @@ async def get_pydev_ccu_central_unit_full(
|
|
|
241
289
|
"""Create and yield central, after all devices have been created."""
|
|
242
290
|
device_event = asyncio.Event()
|
|
243
291
|
|
|
244
|
-
def
|
|
245
|
-
"""Handle
|
|
246
|
-
if event.
|
|
292
|
+
def device_lifecycle_event_handler(event: DeviceLifecycleEvent) -> None:
|
|
293
|
+
"""Handle device lifecycle events."""
|
|
294
|
+
if event.event_type == DeviceLifecycleEventType.CREATED:
|
|
247
295
|
device_event.set()
|
|
248
296
|
|
|
249
297
|
interface_configs = {
|
|
@@ -254,7 +302,7 @@ async def get_pydev_ccu_central_unit_full(
|
|
|
254
302
|
)
|
|
255
303
|
}
|
|
256
304
|
|
|
257
|
-
central = CentralConfig(
|
|
305
|
+
central = await CentralConfig(
|
|
258
306
|
name=const.CENTRAL_NAME,
|
|
259
307
|
host=const.CCU_HOST,
|
|
260
308
|
username=const.CCU_USERNAME,
|
|
@@ -265,7 +313,7 @@ async def get_pydev_ccu_central_unit_full(
|
|
|
265
313
|
program_markers=(),
|
|
266
314
|
sysvar_markers=(),
|
|
267
315
|
).create_central()
|
|
268
|
-
central.event_bus.subscribe(event_type=
|
|
316
|
+
central.event_bus.subscribe(event_type=DeviceLifecycleEvent, event_key=None, handler=device_lifecycle_event_handler)
|
|
269
317
|
await central.start()
|
|
270
318
|
|
|
271
319
|
# Wait up to 60 seconds for the DEVICES_CREATED event which signals that all devices are available
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
1
3
|
"""Helpers for tests."""
|
|
2
4
|
|
|
3
5
|
from __future__ import annotations
|
|
@@ -11,7 +13,7 @@ import orjson
|
|
|
11
13
|
|
|
12
14
|
from aiohomematic.central import CentralUnit
|
|
13
15
|
from aiohomematic.const import UTF_8
|
|
14
|
-
from aiohomematic.interfaces
|
|
16
|
+
from aiohomematic.interfaces import CustomDataPointProtocol
|
|
15
17
|
|
|
16
18
|
_LOGGER = logging.getLogger(__name__)
|
|
17
19
|
|
|
@@ -19,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
19
21
|
# pylint: disable=protected-access
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
def _load_json_file(anchor: str, resource: str, file_name: str) -> Any | None:
|
|
24
|
+
def _load_json_file(anchor: str, resource: str, file_name: str) -> Any | None: # kwonly: disable
|
|
23
25
|
"""Load json file from disk into dict."""
|
|
24
26
|
package_path = str(importlib.resources.files(anchor))
|
|
25
27
|
with open(
|
|
@@ -29,7 +31,7 @@ def _load_json_file(anchor: str, resource: str, file_name: str) -> Any | None:
|
|
|
29
31
|
return orjson.loads(fptr.read())
|
|
30
32
|
|
|
31
33
|
|
|
32
|
-
def get_prepared_custom_data_point(
|
|
34
|
+
def get_prepared_custom_data_point( # kwonly: disable
|
|
33
35
|
central: CentralUnit, address: str, channel_no: int
|
|
34
36
|
) -> CustomDataPointProtocol | None:
|
|
35
37
|
"""Return the hm custom_data_point."""
|
|
@@ -40,7 +42,7 @@ def get_prepared_custom_data_point(
|
|
|
40
42
|
return None
|
|
41
43
|
|
|
42
44
|
|
|
43
|
-
def load_device_description(file_name: str) -> Any:
|
|
45
|
+
def load_device_description(file_name: str) -> Any: # kwonly: disable
|
|
44
46
|
"""Load device description."""
|
|
45
47
|
dev_desc = _load_json_file(anchor="pydevccu", resource="device_descriptions", file_name=file_name)
|
|
46
48
|
assert dev_desc
|