aiohomematic-test-support 2025.12.42__py3-none-any.whl → 2026.1.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.
- aiohomematic_test_support/__init__.py +32 -1
- aiohomematic_test_support/const.py +2 -0
- aiohomematic_test_support/event_capture.py +257 -0
- aiohomematic_test_support/event_mock.py +300 -0
- aiohomematic_test_support/factory.py +23 -13
- aiohomematic_test_support/helper.py +6 -4
- aiohomematic_test_support/mock.py +32 -8
- {aiohomematic_test_support-2025.12.42.dist-info → aiohomematic_test_support-2026.1.40.dist-info}/METADATA +2 -2
- aiohomematic_test_support-2026.1.40.dist-info/RECORD +15 -0
- aiohomematic_test_support-2025.12.42.dist-info/RECORD +0 -13
- {aiohomematic_test_support-2025.12.42.dist-info → aiohomematic_test_support-2026.1.40.dist-info}/WHEEL +0 -0
- {aiohomematic_test_support-2025.12.42.dist-info → aiohomematic_test_support-2026.1.40.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
1
3
|
"""
|
|
2
4
|
Test support infrastructure for aiohomematic.
|
|
3
5
|
|
|
@@ -16,6 +18,8 @@ Key Components
|
|
|
16
18
|
playback support for deterministic testing.
|
|
17
19
|
- **helper**: Test helper utilities for common testing operations.
|
|
18
20
|
- **const**: Test-specific constants and configuration values.
|
|
21
|
+
- **event_capture**: Event capture and assertion utilities for behavior testing.
|
|
22
|
+
- **event_mock**: Event-driven mock server for test triggers.
|
|
19
23
|
|
|
20
24
|
Usage Example
|
|
21
25
|
-------------
|
|
@@ -33,6 +37,33 @@ Using the factory to create a test central with session playback:
|
|
|
33
37
|
device = central.device_coordinator.get_device_by_address("VCU0000001")
|
|
34
38
|
await central.stop()
|
|
35
39
|
|
|
40
|
+
Using EventCapture for behavior-focused testing:
|
|
41
|
+
|
|
42
|
+
from aiohomematic_test_support.event_capture import EventCapture
|
|
43
|
+
from aiohomematic.central.events import CircuitBreakerTrippedEvent
|
|
44
|
+
|
|
45
|
+
capture = EventCapture()
|
|
46
|
+
capture.subscribe_to(central.event_bus, CircuitBreakerTrippedEvent)
|
|
47
|
+
|
|
48
|
+
# ... trigger failures ...
|
|
49
|
+
|
|
50
|
+
capture.assert_event_emitted(CircuitBreakerTrippedEvent, failure_count=5)
|
|
51
|
+
capture.cleanup()
|
|
52
|
+
|
|
53
|
+
Using EventDrivenMockServer for event-triggered test behavior:
|
|
54
|
+
|
|
55
|
+
from aiohomematic_test_support.event_mock import EventDrivenMockServer
|
|
56
|
+
from aiohomematic.central.events import DataRefreshTriggeredEvent
|
|
57
|
+
|
|
58
|
+
mock_server = EventDrivenMockServer(event_bus=central.event_bus)
|
|
59
|
+
mock_server.when(DataRefreshTriggeredEvent).then_call(
|
|
60
|
+
lambda event: inject_mock_data()
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# ... trigger refresh ...
|
|
64
|
+
|
|
65
|
+
mock_server.cleanup()
|
|
66
|
+
|
|
36
67
|
The session player replays pre-recorded backend responses, enabling fast and
|
|
37
68
|
reproducible tests without backend dependencies.
|
|
38
69
|
|
|
@@ -45,4 +76,4 @@ test dependencies to access test support functionality.
|
|
|
45
76
|
|
|
46
77
|
from __future__ import annotations
|
|
47
78
|
|
|
48
|
-
__version__ = "
|
|
79
|
+
__version__ = "2026.1.40"
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Event capture utilities for test verification.
|
|
5
|
+
|
|
6
|
+
This module provides tools for capturing and asserting events in tests,
|
|
7
|
+
enabling behavior-focused testing through event verification.
|
|
8
|
+
|
|
9
|
+
Public API
|
|
10
|
+
----------
|
|
11
|
+
- EventCapture: Capture events for test verification
|
|
12
|
+
- EventSequenceAssertion: Verify event ordering
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from aiohomematic.central.events import Event, EventBus
|
|
23
|
+
|
|
24
|
+
T = TypeVar("T", bound="Event")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class EventCapture:
|
|
29
|
+
"""
|
|
30
|
+
Capture events from EventBus for test verification.
|
|
31
|
+
|
|
32
|
+
Provides methods to subscribe to events, capture them, and make assertions
|
|
33
|
+
about what events were emitted during test execution.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
-------
|
|
37
|
+
capture = EventCapture()
|
|
38
|
+
capture.subscribe_to(event_bus, CircuitBreakerTrippedEvent)
|
|
39
|
+
|
|
40
|
+
# ... perform test actions ...
|
|
41
|
+
|
|
42
|
+
capture.assert_event_emitted(
|
|
43
|
+
CircuitBreakerTrippedEvent,
|
|
44
|
+
interface_id="test",
|
|
45
|
+
failure_count=5,
|
|
46
|
+
)
|
|
47
|
+
capture.cleanup()
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
captured_events: list[Event] = field(default_factory=list)
|
|
52
|
+
"""List of all captured events."""
|
|
53
|
+
|
|
54
|
+
_unsubscribers: list[Callable[[], None]] = field(default_factory=list)
|
|
55
|
+
"""Unsubscribe callbacks for cleanup."""
|
|
56
|
+
|
|
57
|
+
def assert_event_emitted(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
event_type: type[Event],
|
|
61
|
+
count: int | None = None,
|
|
62
|
+
**expected_attrs: Any,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Assert that an event with specific attributes was emitted.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
event_type: The expected event type
|
|
69
|
+
count: If provided, assert exactly this many events were emitted
|
|
70
|
+
**expected_attrs: Expected attribute values on the event
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
AssertionError: If no matching event found or count mismatch
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
events = self.get_events_of_type(event_type=event_type)
|
|
77
|
+
|
|
78
|
+
if count is not None and len(events) != count:
|
|
79
|
+
raise AssertionError(f"Expected {count} {event_type.__name__} events, got {len(events)}")
|
|
80
|
+
|
|
81
|
+
if not expected_attrs:
|
|
82
|
+
if not events:
|
|
83
|
+
raise AssertionError(f"No {event_type.__name__} events captured")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
for event in events:
|
|
87
|
+
if all(getattr(event, k, None) == v for k, v in expected_attrs.items()):
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
raise AssertionError(
|
|
91
|
+
f"No {event_type.__name__} found with attributes {expected_attrs}. Captured {len(events)} events: {events}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def assert_no_event(self, *, event_type: type[Event]) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Assert that no events of a specific type were emitted.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
event_type: The event type that should not exist
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
AssertionError: If any events of that type were captured
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
if events := self.get_events_of_type(event_type=event_type):
|
|
106
|
+
raise AssertionError(f"Expected no {event_type.__name__} events, but found {len(events)}: {events}")
|
|
107
|
+
|
|
108
|
+
def cleanup(self) -> None:
|
|
109
|
+
"""Unsubscribe from all events and clear captured events."""
|
|
110
|
+
for unsub in self._unsubscribers:
|
|
111
|
+
unsub()
|
|
112
|
+
self._unsubscribers.clear()
|
|
113
|
+
self.captured_events.clear()
|
|
114
|
+
|
|
115
|
+
def clear(self) -> None:
|
|
116
|
+
"""Clear all captured events."""
|
|
117
|
+
self.captured_events.clear()
|
|
118
|
+
|
|
119
|
+
def get_event_count(self, *, event_type: type[Event]) -> int:
|
|
120
|
+
"""
|
|
121
|
+
Return count of captured events of a specific type.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
event_type: The event type to count
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Number of events of that type
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
return len(self.get_events_of_type(event_type=event_type))
|
|
131
|
+
|
|
132
|
+
def get_events_of_type(self, *, event_type: type[T]) -> list[T]:
|
|
133
|
+
"""
|
|
134
|
+
Return all captured events of a specific type.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
event_type: The event type to filter by
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
List of events matching the type
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
return [e for e in self.captured_events if isinstance(e, event_type)]
|
|
144
|
+
|
|
145
|
+
def subscribe_to(self, event_bus: EventBus, *event_types: type[Event]) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Subscribe to specific event types on the EventBus.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
event_bus: The EventBus to subscribe to
|
|
151
|
+
*event_types: Event types to capture
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
for event_type in event_types:
|
|
155
|
+
self._unsubscribers.append(
|
|
156
|
+
event_bus.subscribe(
|
|
157
|
+
event_type=event_type,
|
|
158
|
+
event_key=None,
|
|
159
|
+
handler=self._capture_handler,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def _capture_handler(self, *, event: Event) -> None:
|
|
164
|
+
"""Handle event by capturing it."""
|
|
165
|
+
self.captured_events.append(event)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class EventSequenceAssertion:
|
|
170
|
+
"""
|
|
171
|
+
Assert events occur in expected sequence.
|
|
172
|
+
|
|
173
|
+
Useful for verifying state machine transitions and multi-step processes.
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
-------
|
|
177
|
+
sequence = EventSequenceAssertion(expected_sequence=[
|
|
178
|
+
ConnectionStageChangedEvent,
|
|
179
|
+
ClientStateChangedEvent,
|
|
180
|
+
CentralStateChangedEvent,
|
|
181
|
+
])
|
|
182
|
+
event_bus.subscribe(Event, sequence.on_event)
|
|
183
|
+
|
|
184
|
+
# ... perform actions ...
|
|
185
|
+
|
|
186
|
+
sequence.verify()
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
expected_sequence: list[type[Event]]
|
|
191
|
+
"""The expected sequence of event types."""
|
|
192
|
+
|
|
193
|
+
captured_types: list[type[Event]] = field(default_factory=list)
|
|
194
|
+
"""Captured event types in order."""
|
|
195
|
+
|
|
196
|
+
def on_event(self, *, event: Event) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Handle event by capturing its type.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
event: The event that was published
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
self.captured_types.append(type(event))
|
|
205
|
+
|
|
206
|
+
def reset(self) -> None:
|
|
207
|
+
"""Reset captured types for reuse."""
|
|
208
|
+
self.captured_types.clear()
|
|
209
|
+
|
|
210
|
+
def verify(self, *, strict: bool = False) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Verify the captured sequence matches expected.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
strict: If True, require exact sequence match.
|
|
216
|
+
If False, verify expected events appear in order
|
|
217
|
+
(other events may be interspersed).
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
AssertionError: If sequence doesn't match
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
if strict:
|
|
224
|
+
self._verify_strict()
|
|
225
|
+
else:
|
|
226
|
+
self._verify_subsequence()
|
|
227
|
+
|
|
228
|
+
def _verify_strict(self) -> None:
|
|
229
|
+
"""Verify exact sequence match."""
|
|
230
|
+
if len(self.captured_types) != len(self.expected_sequence):
|
|
231
|
+
raise AssertionError(
|
|
232
|
+
f"Expected {len(self.expected_sequence)} events, "
|
|
233
|
+
f"got {len(self.captured_types)}.\n"
|
|
234
|
+
f"Expected: {[e.__name__ for e in self.expected_sequence]}\n"
|
|
235
|
+
f"Got: {[e.__name__ for e in self.captured_types]}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
for i, (expected, actual) in enumerate(zip(self.expected_sequence, self.captured_types, strict=True)):
|
|
239
|
+
if expected != actual:
|
|
240
|
+
raise AssertionError(f"Event {i}: expected {expected.__name__}, got {actual.__name__}")
|
|
241
|
+
|
|
242
|
+
def _verify_subsequence(self) -> None:
|
|
243
|
+
"""Verify expected events appear in order (non-strict)."""
|
|
244
|
+
expected_idx = 0
|
|
245
|
+
for actual_type in self.captured_types:
|
|
246
|
+
if expected_idx >= len(self.expected_sequence):
|
|
247
|
+
break
|
|
248
|
+
if actual_type == self.expected_sequence[expected_idx]:
|
|
249
|
+
expected_idx += 1
|
|
250
|
+
|
|
251
|
+
if expected_idx < len(self.expected_sequence):
|
|
252
|
+
missing = self.expected_sequence[expected_idx:]
|
|
253
|
+
raise AssertionError(
|
|
254
|
+
f"Missing expected events in sequence: {[e.__name__ for e in missing]}\n"
|
|
255
|
+
f"Expected: {[e.__name__ for e in self.expected_sequence]}\n"
|
|
256
|
+
f"Got: {[e.__name__ for e in self.captured_types]}"
|
|
257
|
+
)
|
|
@@ -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
|
|
|
@@ -41,16 +43,16 @@ from unittest.mock import MagicMock, Mock, patch
|
|
|
41
43
|
from aiohttp import ClientSession
|
|
42
44
|
|
|
43
45
|
from aiohomematic.central import CentralConfig, CentralUnit
|
|
44
|
-
from aiohomematic.central.
|
|
46
|
+
from aiohomematic.central.events import (
|
|
45
47
|
DataPointsCreatedEvent,
|
|
46
48
|
DeviceLifecycleEvent,
|
|
47
49
|
DeviceLifecycleEventType,
|
|
48
50
|
DeviceTriggerEvent,
|
|
49
|
-
|
|
51
|
+
SystemStatusChangedEvent,
|
|
50
52
|
)
|
|
51
|
-
from aiohomematic.client import
|
|
53
|
+
from aiohomematic.client import InterfaceConfig, create_client as create_client_func
|
|
52
54
|
from aiohomematic.const import LOCAL_HOST, Interface, OptionalSettings
|
|
53
|
-
from aiohomematic.interfaces
|
|
55
|
+
from aiohomematic.interfaces import ClientProtocol
|
|
54
56
|
from aiohomematic_test_support import const
|
|
55
57
|
from aiohomematic_test_support.mock import SessionPlayer, get_client_session, get_mock, get_xml_rpc_proxy
|
|
56
58
|
|
|
@@ -116,10 +118,17 @@ class FactoryWithClient:
|
|
|
116
118
|
|
|
117
119
|
# Optionally patch client creation to return a mocked client
|
|
118
120
|
if self._do_mock_client:
|
|
119
|
-
_orig_create_client =
|
|
120
|
-
|
|
121
|
-
async def _mocked_create_client(
|
|
122
|
-
|
|
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
|
+
)
|
|
123
132
|
return cast(
|
|
124
133
|
Mock,
|
|
125
134
|
get_mock(
|
|
@@ -129,7 +138,7 @@ class FactoryWithClient:
|
|
|
129
138
|
),
|
|
130
139
|
)
|
|
131
140
|
|
|
132
|
-
p3 = patch("aiohomematic.client.
|
|
141
|
+
p3 = patch("aiohomematic.client.create_client", _mocked_create_client)
|
|
133
142
|
p3.start()
|
|
134
143
|
self._patches.append(p3)
|
|
135
144
|
|
|
@@ -142,7 +151,7 @@ class FactoryWithClient:
|
|
|
142
151
|
async def get_raw_central(self) -> CentralUnit:
|
|
143
152
|
"""Return a central based on give address_device_translation."""
|
|
144
153
|
interface_configs = self._interface_configs if self._interface_configs else set()
|
|
145
|
-
central = CentralConfig(
|
|
154
|
+
central = await CentralConfig(
|
|
146
155
|
name=const.CENTRAL_NAME,
|
|
147
156
|
host=const.CCU_HOST,
|
|
148
157
|
username=const.CCU_USERNAME,
|
|
@@ -169,7 +178,7 @@ class FactoryWithClient:
|
|
|
169
178
|
"""Handle device trigger events."""
|
|
170
179
|
self.ha_event_mock(event)
|
|
171
180
|
|
|
172
|
-
def _system_status_event_handler(event:
|
|
181
|
+
def _system_status_event_handler(event: SystemStatusChangedEvent) -> None:
|
|
173
182
|
"""Handle system status events (issues, state changes)."""
|
|
174
183
|
self.ha_event_mock(event)
|
|
175
184
|
|
|
@@ -190,7 +199,7 @@ class FactoryWithClient:
|
|
|
190
199
|
)
|
|
191
200
|
self._event_bus_unsubscribe_callbacks.append(
|
|
192
201
|
central.event_bus.subscribe(
|
|
193
|
-
event_type=
|
|
202
|
+
event_type=SystemStatusChangedEvent, event_key=None, handler=_system_status_event_handler
|
|
194
203
|
)
|
|
195
204
|
)
|
|
196
205
|
|
|
@@ -293,7 +302,7 @@ async def get_pydev_ccu_central_unit_full(
|
|
|
293
302
|
)
|
|
294
303
|
}
|
|
295
304
|
|
|
296
|
-
central = CentralConfig(
|
|
305
|
+
central = await CentralConfig(
|
|
297
306
|
name=const.CENTRAL_NAME,
|
|
298
307
|
host=const.CCU_HOST,
|
|
299
308
|
username=const.CCU_USERNAME,
|
|
@@ -303,6 +312,7 @@ async def get_pydev_ccu_central_unit_full(
|
|
|
303
312
|
client_session=client_session,
|
|
304
313
|
program_markers=(),
|
|
305
314
|
sysvar_markers=(),
|
|
315
|
+
start_direct=True,
|
|
306
316
|
).create_central()
|
|
307
317
|
central.event_bus.subscribe(event_type=DeviceLifecycleEvent, event_key=None, handler=device_lifecycle_event_handler)
|
|
308
318
|
await central.start()
|
|
@@ -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
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
1
3
|
"""
|
|
2
4
|
Mock implementations for RPC clients with session playback.
|
|
3
5
|
|
|
@@ -50,12 +52,14 @@ import zipfile
|
|
|
50
52
|
from aiohttp import ClientSession
|
|
51
53
|
import orjson
|
|
52
54
|
|
|
55
|
+
from aiohomematic.async_support import Looper
|
|
53
56
|
from aiohomematic.central import CentralUnit
|
|
54
|
-
from aiohomematic.client import BaseRpcProxy
|
|
57
|
+
from aiohomematic.client import BaseRpcProxy, CircuitBreaker
|
|
55
58
|
from aiohomematic.client.json_rpc import _JsonKey, _JsonRpcMethod
|
|
56
59
|
from aiohomematic.client.rpc_proxy import _RpcMethod
|
|
57
60
|
from aiohomematic.const import UTF_8, DataOperationResult, Parameter, ParamsetKey, RPCType
|
|
58
|
-
from aiohomematic.
|
|
61
|
+
from aiohomematic.property_decorators import DelegatedProperty
|
|
62
|
+
from aiohomematic.store import cleanup_params_for_session, freeze_params, unfreeze_params
|
|
59
63
|
from aiohomematic_test_support import const
|
|
60
64
|
|
|
61
65
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -66,6 +70,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
66
70
|
def _get_not_mockable_method_names(*, instance: Any, exclude_methods: set[str]) -> set[str]:
|
|
67
71
|
"""Return all relevant method names for mocking."""
|
|
68
72
|
methods: set[str] = set(_get_properties(data_object=instance, decorator=property))
|
|
73
|
+
methods |= _get_properties(data_object=instance, decorator=DelegatedProperty)
|
|
69
74
|
|
|
70
75
|
for method in dir(instance):
|
|
71
76
|
if method in exclude_methods:
|
|
@@ -186,6 +191,16 @@ def get_client_session( # noqa: C901
|
|
|
186
191
|
}
|
|
187
192
|
)
|
|
188
193
|
|
|
194
|
+
if "fetch_all_device_data" in params[_JsonKey.SCRIPT]:
|
|
195
|
+
# Return empty device data dict for InterfaceClient
|
|
196
|
+
return _MockResponse(
|
|
197
|
+
json_data={
|
|
198
|
+
_JsonKey.ID: 0,
|
|
199
|
+
_JsonKey.RESULT: {},
|
|
200
|
+
_JsonKey.ERROR: None,
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
|
|
189
204
|
if method == _JsonRpcMethod.INTERFACE_SET_VALUE:
|
|
190
205
|
await self._central.event_coordinator.data_point_event(
|
|
191
206
|
interface_id=params[_JsonKey.INTERFACE],
|
|
@@ -270,11 +285,18 @@ def get_xml_rpc_proxy( # noqa: C901
|
|
|
270
285
|
self._player = player
|
|
271
286
|
self._supported_methods: tuple[str, ...] = ()
|
|
272
287
|
self._central: CentralUnit | None = None
|
|
288
|
+
# Real CircuitBreaker to provide actual metrics for tests
|
|
289
|
+
self._circuit_breaker = CircuitBreaker(interface_id="mock-interface", task_scheduler=Looper())
|
|
273
290
|
|
|
274
291
|
def __getattr__(self, name: str) -> Any:
|
|
275
292
|
# Start of method chain
|
|
276
293
|
return _Method(name, self._invoke)
|
|
277
294
|
|
|
295
|
+
@property
|
|
296
|
+
def circuit_breaker(self) -> CircuitBreaker:
|
|
297
|
+
"""Return the circuit breaker for metrics access."""
|
|
298
|
+
return self._circuit_breaker
|
|
299
|
+
|
|
278
300
|
@property
|
|
279
301
|
def supported_methods(self) -> tuple[str, ...]:
|
|
280
302
|
"""Return the supported methods."""
|
|
@@ -322,11 +344,11 @@ def get_xml_rpc_proxy( # noqa: C901
|
|
|
322
344
|
|
|
323
345
|
for dd in devices:
|
|
324
346
|
if ignore_devices_on_create is not None and (
|
|
325
|
-
dd["ADDRESS"] in ignore_devices_on_create or dd
|
|
347
|
+
dd["ADDRESS"] in ignore_devices_on_create or dd.get("PARENT") in ignore_devices_on_create
|
|
326
348
|
):
|
|
327
349
|
continue
|
|
328
350
|
if address_device_translation is not None:
|
|
329
|
-
if dd["ADDRESS"] in address_device_translation or dd
|
|
351
|
+
if dd["ADDRESS"] in address_device_translation or dd.get("PARENT") in address_device_translation:
|
|
330
352
|
new_devices.append(dd)
|
|
331
353
|
else:
|
|
332
354
|
new_devices.append(dd)
|
|
@@ -382,7 +404,7 @@ def get_xml_rpc_proxy( # noqa: C901
|
|
|
382
404
|
return cast(BaseRpcProxy, _AioXmlRpcProxyFromSession())
|
|
383
405
|
|
|
384
406
|
|
|
385
|
-
def _get_instance_attributes(instance: Any) -> set[str]:
|
|
407
|
+
def _get_instance_attributes(instance: Any) -> set[str]: # kwonly: disable
|
|
386
408
|
"""
|
|
387
409
|
Get all instance attribute names, supporting both __dict__ and __slots__.
|
|
388
410
|
|
|
@@ -479,9 +501,11 @@ def get_mock(
|
|
|
479
501
|
setattr(instance, attr, getattr(instance._mock_wraps, attr))
|
|
480
502
|
return instance
|
|
481
503
|
|
|
482
|
-
# Step 1: Identify all @property decorated attributes on the class
|
|
504
|
+
# Step 1: Identify all @property and DelegatedProperty decorated attributes on the class
|
|
483
505
|
# These need special handling because MagicMock doesn't delegate property access
|
|
484
506
|
property_names = _get_properties(data_object=instance, decorator=property)
|
|
507
|
+
# Also include DelegatedProperty descriptors which are now used extensively
|
|
508
|
+
property_names |= _get_properties(data_object=instance, decorator=DelegatedProperty)
|
|
485
509
|
|
|
486
510
|
# Step 2: Create a dynamic MagicMock subclass
|
|
487
511
|
# We add property descriptors to this subclass that delegate to _mock_wraps.
|
|
@@ -644,7 +668,7 @@ class SessionPlayer:
|
|
|
644
668
|
except ValueError:
|
|
645
669
|
continue
|
|
646
670
|
resp = bucket_by_ts[latest_ts]
|
|
647
|
-
params =
|
|
671
|
+
params = unfreeze_params(frozen_params=frozen_params)
|
|
648
672
|
|
|
649
673
|
result.append((params, resp))
|
|
650
674
|
return result
|
|
@@ -689,7 +713,7 @@ class SessionPlayer:
|
|
|
689
713
|
return None
|
|
690
714
|
if not (bucket_by_parameter := bucket_by_method.get(method)):
|
|
691
715
|
return None
|
|
692
|
-
frozen_params =
|
|
716
|
+
frozen_params = freeze_params(params=cleanup_params_for_session(params=params))
|
|
693
717
|
|
|
694
718
|
# For each parameter, choose the response at the latest timestamp.
|
|
695
719
|
if (bucket_by_ts := bucket_by_parameter.get(frozen_params)) is None:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic-test-support
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2026.1.40
|
|
4
4
|
Summary: Support-only package for AioHomematic (tests/dev). Not part of production builds.
|
|
5
|
-
Author-email: SukramJ <sukramj@icloud.com>
|
|
5
|
+
Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/SukramJ/aiohomematic
|
|
7
7
|
Classifier: Development Status :: 3 - Alpha
|
|
8
8
|
Classifier: Intended Audience :: Developers
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
aiohomematic_test_support/__init__.py,sha256=rRlNZwaLwcBmnAdHge8raeNAkF0u_aNSYEM7N8I0i8M,2711
|
|
2
|
+
aiohomematic_test_support/const.py,sha256=cYsT9WMnoxRR1zRRUFX8M7A9e0pwS3NcxKK9smXiiw8,6487
|
|
3
|
+
aiohomematic_test_support/event_capture.py,sha256=9giXt8-vbPXUbG0X89Jt5Q8LfPli0y-liLVVUhiw-EM,8002
|
|
4
|
+
aiohomematic_test_support/event_mock.py,sha256=sWtAqMPXxkWFNqslw2wfgXGtSM0kLMm0Lg_qkkAFE4g,8493
|
|
5
|
+
aiohomematic_test_support/factory.py,sha256=U_-YrtgY-L5fo1WScR7jfCxJmHbJRueWTH9FSsC9ZCQ,12499
|
|
6
|
+
aiohomematic_test_support/helper.py,sha256=r63EpCEPmWFdcxiEDR5k22AwbB112DbWJwK66iHLeU4,1468
|
|
7
|
+
aiohomematic_test_support/mock.py,sha256=GYwpwH-mfaobK8DyNj3FFC2zyvATs46sx2l7V5wnqGA,32366
|
|
8
|
+
aiohomematic_test_support/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
aiohomematic_test_support/data/device_translation.json,sha256=owHLpCGvtMJzfCMOMMgnn7-LgN-BMKSYzi4NKINw1WQ,13290
|
|
10
|
+
aiohomematic_test_support/data/full_session_randomized_ccu.zip,sha256=kKHMtJouCskAHkXaP1hHRSoWZO2cZDJ9P_EwuJuywJQ,733017
|
|
11
|
+
aiohomematic_test_support/data/full_session_randomized_pydevccu.zip,sha256=_QFWSP03dkiMFdD_w-R98DS6ur4PYDQXw-DCkbJEGg4,1293240
|
|
12
|
+
aiohomematic_test_support-2026.1.40.dist-info/METADATA,sha256=-UIyk1-RVR65AyJBlGgT7MAL6rci2WIEA5ozy9_OOeQ,575
|
|
13
|
+
aiohomematic_test_support-2026.1.40.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
aiohomematic_test_support-2026.1.40.dist-info/top_level.txt,sha256=KmK-OiDDbrmawIsIgPWNAkpkDfWQnOoumYd9MXAiTHc,26
|
|
15
|
+
aiohomematic_test_support-2026.1.40.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
aiohomematic_test_support/__init__.py,sha256=_61m3I7tUF-q8k6-Vh3nz81vkCAY_W-UUcyhRrTeViw,1642
|
|
2
|
-
aiohomematic_test_support/const.py,sha256=tLyvqZJlaDCs1F2OBRnJ94hUB0pu7RKk--CQGlVC_58,6430
|
|
3
|
-
aiohomematic_test_support/factory.py,sha256=FoemfxruooVRViShZdW4uxHc09pCIUFSCG_goxvpVmg,12191
|
|
4
|
-
aiohomematic_test_support/helper.py,sha256=Ue2tfy10_fiuMjYsc1jYPvo5sEtMF2WVKjvLnTZ0TzU,1360
|
|
5
|
-
aiohomematic_test_support/mock.py,sha256=GhJjKXPIYwzmHanKQSEIjGb8-wuixa3n7cFW-m-oiKY,31097
|
|
6
|
-
aiohomematic_test_support/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
aiohomematic_test_support/data/device_translation.json,sha256=owHLpCGvtMJzfCMOMMgnn7-LgN-BMKSYzi4NKINw1WQ,13290
|
|
8
|
-
aiohomematic_test_support/data/full_session_randomized_ccu.zip,sha256=kKHMtJouCskAHkXaP1hHRSoWZO2cZDJ9P_EwuJuywJQ,733017
|
|
9
|
-
aiohomematic_test_support/data/full_session_randomized_pydevccu.zip,sha256=_QFWSP03dkiMFdD_w-R98DS6ur4PYDQXw-DCkbJEGg4,1293240
|
|
10
|
-
aiohomematic_test_support-2025.12.42.dist-info/METADATA,sha256=2h9S5x7EQqUEeN0oSnPGMuU8tYvv60HjCWP9hNiGe04,536
|
|
11
|
-
aiohomematic_test_support-2025.12.42.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
aiohomematic_test_support-2025.12.42.dist-info/top_level.txt,sha256=KmK-OiDDbrmawIsIgPWNAkpkDfWQnOoumYd9MXAiTHc,26
|
|
13
|
-
aiohomematic_test_support-2025.12.42.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|