lifx-emulator 2.3.1__py3-none-any.whl → 3.0.1__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.
Files changed (68) hide show
  1. lifx_emulator-3.0.1.dist-info/METADATA +102 -0
  2. lifx_emulator-3.0.1.dist-info/RECORD +18 -0
  3. lifx_emulator-3.0.1.dist-info/entry_points.txt +2 -0
  4. lifx_emulator_app/__init__.py +10 -0
  5. {lifx_emulator → lifx_emulator_app}/__main__.py +13 -5
  6. {lifx_emulator → lifx_emulator_app}/api/__init__.py +1 -1
  7. {lifx_emulator → lifx_emulator_app}/api/app.py +3 -3
  8. {lifx_emulator → lifx_emulator_app}/api/mappers/__init__.py +1 -1
  9. {lifx_emulator → lifx_emulator_app}/api/mappers/device_mapper.py +1 -1
  10. {lifx_emulator → lifx_emulator_app}/api/models.py +1 -2
  11. lifx_emulator_app/api/routers/__init__.py +11 -0
  12. {lifx_emulator → lifx_emulator_app}/api/routers/devices.py +2 -2
  13. {lifx_emulator → lifx_emulator_app}/api/routers/monitoring.py +1 -1
  14. {lifx_emulator → lifx_emulator_app}/api/routers/scenarios.py +1 -1
  15. lifx_emulator_app/api/services/__init__.py +8 -0
  16. {lifx_emulator → lifx_emulator_app}/api/services/device_service.py +3 -2
  17. lifx_emulator/__init__.py +0 -31
  18. lifx_emulator/api/routers/__init__.py +0 -11
  19. lifx_emulator/api/services/__init__.py +0 -8
  20. lifx_emulator/constants.py +0 -33
  21. lifx_emulator/devices/__init__.py +0 -37
  22. lifx_emulator/devices/device.py +0 -339
  23. lifx_emulator/devices/manager.py +0 -256
  24. lifx_emulator/devices/observers.py +0 -139
  25. lifx_emulator/devices/persistence.py +0 -308
  26. lifx_emulator/devices/state_restorer.py +0 -259
  27. lifx_emulator/devices/state_serializer.py +0 -157
  28. lifx_emulator/devices/states.py +0 -377
  29. lifx_emulator/factories/__init__.py +0 -37
  30. lifx_emulator/factories/builder.py +0 -373
  31. lifx_emulator/factories/default_config.py +0 -158
  32. lifx_emulator/factories/factory.py +0 -221
  33. lifx_emulator/factories/firmware_config.py +0 -77
  34. lifx_emulator/factories/serial_generator.py +0 -82
  35. lifx_emulator/handlers/__init__.py +0 -39
  36. lifx_emulator/handlers/base.py +0 -49
  37. lifx_emulator/handlers/device_handlers.py +0 -322
  38. lifx_emulator/handlers/light_handlers.py +0 -503
  39. lifx_emulator/handlers/multizone_handlers.py +0 -249
  40. lifx_emulator/handlers/registry.py +0 -110
  41. lifx_emulator/handlers/tile_handlers.py +0 -488
  42. lifx_emulator/products/__init__.py +0 -28
  43. lifx_emulator/products/generator.py +0 -1037
  44. lifx_emulator/products/registry.py +0 -1496
  45. lifx_emulator/products/specs.py +0 -284
  46. lifx_emulator/products/specs.yml +0 -352
  47. lifx_emulator/protocol/__init__.py +0 -1
  48. lifx_emulator/protocol/base.py +0 -446
  49. lifx_emulator/protocol/const.py +0 -8
  50. lifx_emulator/protocol/generator.py +0 -1384
  51. lifx_emulator/protocol/header.py +0 -159
  52. lifx_emulator/protocol/packets.py +0 -1351
  53. lifx_emulator/protocol/protocol_types.py +0 -817
  54. lifx_emulator/protocol/serializer.py +0 -379
  55. lifx_emulator/repositories/__init__.py +0 -22
  56. lifx_emulator/repositories/device_repository.py +0 -155
  57. lifx_emulator/repositories/storage_backend.py +0 -107
  58. lifx_emulator/scenarios/__init__.py +0 -22
  59. lifx_emulator/scenarios/manager.py +0 -322
  60. lifx_emulator/scenarios/models.py +0 -112
  61. lifx_emulator/scenarios/persistence.py +0 -241
  62. lifx_emulator/server.py +0 -464
  63. lifx_emulator-2.3.1.dist-info/METADATA +0 -107
  64. lifx_emulator-2.3.1.dist-info/RECORD +0 -62
  65. lifx_emulator-2.3.1.dist-info/entry_points.txt +0 -2
  66. lifx_emulator-2.3.1.dist-info/licenses/LICENSE +0 -35
  67. {lifx_emulator-2.3.1.dist-info → lifx_emulator-3.0.1.dist-info}/WHEEL +0 -0
  68. {lifx_emulator → lifx_emulator_app}/api/templates/dashboard.html +0 -0
@@ -1,339 +0,0 @@
1
- """Device state and emulated device implementation."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import copy
7
- import logging
8
- import time
9
- from typing import Any
10
-
11
- from lifx_emulator.constants import LIFX_HEADER_SIZE
12
- from lifx_emulator.devices.states import DeviceState, TileFramebuffers
13
- from lifx_emulator.handlers import HandlerRegistry, create_default_registry
14
- from lifx_emulator.protocol.header import LifxHeader
15
- from lifx_emulator.protocol.packets import (
16
- Device,
17
- )
18
- from lifx_emulator.protocol.protocol_types import LightHsbk
19
- from lifx_emulator.scenarios import (
20
- HierarchicalScenarioManager,
21
- ScenarioConfig,
22
- get_device_type,
23
- )
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
- # Forward declaration for type hinting
28
- TYPE_CHECKING = False
29
- if TYPE_CHECKING:
30
- from lifx_emulator.devices.persistence import DevicePersistenceAsyncFile
31
-
32
-
33
- class EmulatedLifxDevice:
34
- """Emulated LIFX device with configurable scenarios and state management."""
35
-
36
- def __init__(
37
- self,
38
- device_state: DeviceState,
39
- storage: DevicePersistenceAsyncFile | None = None,
40
- handler_registry: HandlerRegistry | None = None,
41
- scenario_manager: HierarchicalScenarioManager | None = None,
42
- ):
43
- self.state = device_state
44
- # Use provided scenario manager or create a default empty one
45
- if scenario_manager is not None:
46
- self.scenario_manager = scenario_manager
47
- else:
48
- self.scenario_manager = HierarchicalScenarioManager()
49
- self.start_time = time.time()
50
- self.storage = storage
51
-
52
- # Scenario caching for performance (HierarchicalScenarioManager only)
53
- self._cached_scenario: ScenarioConfig | None = None
54
-
55
- # Track background save tasks to prevent garbage collection
56
- self.background_save_tasks: set[asyncio.Task] = set()
57
-
58
- # Use provided registry or create default one
59
- self.handlers = handler_registry or create_default_registry()
60
-
61
- # Pre-allocate response header template for performance (10-15% gain)
62
- # This avoids creating a new LifxHeader object for every response
63
- self._response_header_template = LifxHeader(
64
- source=0,
65
- target=self.state.get_target_bytes(),
66
- sequence=0,
67
- tagged=False,
68
- pkt_type=0,
69
- size=0,
70
- )
71
-
72
- # Initialize multizone colors if needed
73
- # Note: State restoration is handled by StateRestorer in factories
74
- if self.state.has_multizone and self.state.zone_count > 0:
75
- if not self.state.zone_colors:
76
- # Initialize with rainbow pattern using list comprehension
77
- # (performance optimization)
78
- self.state.zone_colors = [
79
- LightHsbk(
80
- hue=int((i / self.state.zone_count) * 65535),
81
- saturation=65535,
82
- brightness=32768,
83
- kelvin=3500,
84
- )
85
- for i in range(self.state.zone_count)
86
- ]
87
-
88
- # Initialize tile state if needed
89
- # Note: Saved tile data is restored by StateRestorer in factories
90
- if self.state.has_matrix and self.state.tile_count > 0:
91
- if not self.state.tile_devices:
92
- for i in range(self.state.tile_count):
93
- zones = self.state.tile_width * self.state.tile_height
94
- tile_colors = [
95
- LightHsbk(hue=0, saturation=0, brightness=32768, kelvin=3500)
96
- for _ in range(zones)
97
- ]
98
-
99
- self.state.tile_devices.append(
100
- {
101
- "accel_meas_x": 0,
102
- "accel_meas_y": 0,
103
- "accel_meas_z": 0,
104
- "user_x": float(i * self.state.tile_width),
105
- "user_y": 0.0,
106
- "width": self.state.tile_width,
107
- "height": self.state.tile_height,
108
- "device_version_vendor": 1,
109
- "device_version_product": self.state.product,
110
- "firmware_build": int(time.time()),
111
- "firmware_version_minor": 70,
112
- "firmware_version_major": 3,
113
- "colors": tile_colors,
114
- }
115
- )
116
-
117
- # Initialize framebuffer storage for each tile (framebuffers 1-7)
118
- # Framebuffer 0 is stored in tile_devices[i]["colors"]
119
- if not self.state.tile_framebuffers:
120
- for i in range(self.state.tile_count):
121
- self.state.tile_framebuffers.append(TileFramebuffers(tile_index=i))
122
-
123
- # Save initial state if persistence is enabled
124
- # This ensures newly created devices are immediately persisted
125
- if self.storage:
126
- self._save_state()
127
-
128
- def get_uptime_ns(self) -> int:
129
- """Calculate current uptime in nanoseconds"""
130
- return int((time.time() - self.start_time) * 1e9)
131
-
132
- def _save_state(self) -> None:
133
- """Save device state asynchronously (non-blocking).
134
-
135
- Creates a background task to save state without blocking the event loop.
136
- The task is tracked to prevent garbage collection.
137
-
138
- Note: Only DevicePersistenceAsyncFile is supported in production. For testing,
139
- you can pass None to disable persistence.
140
- """
141
- if not self.storage:
142
- return
143
-
144
- try:
145
- loop = asyncio.get_running_loop()
146
- task = loop.create_task(self.storage.save_device_state(self.state))
147
- self._track_save_task(task)
148
- except RuntimeError:
149
- # No event loop (shouldn't happen in normal operation)
150
- logger.error("Cannot save state for %s: no event loop", self.state.serial)
151
-
152
- def _track_save_task(self, task: asyncio.Task) -> None:
153
- """Track background save task to prevent garbage collection.
154
-
155
- Args:
156
- task: Save task to track
157
- """
158
- self.background_save_tasks.add(task)
159
- task.add_done_callback(self.background_save_tasks.discard)
160
-
161
- def _get_resolved_scenario(self) -> ScenarioConfig:
162
- """Get resolved scenario configuration with caching.
163
-
164
- Resolves scenario from all applicable scopes and caches the result
165
- for performance.
166
-
167
- Returns:
168
- ScenarioConfig with resolved settings
169
- """
170
- if self._cached_scenario is not None:
171
- return self._cached_scenario
172
-
173
- # Resolve scenario with hierarchical scoping
174
- self._cached_scenario = self.scenario_manager.get_scenario_for_device(
175
- serial=self.state.serial,
176
- device_type=get_device_type(self),
177
- location=self.state.location_label,
178
- group=self.state.group_label,
179
- )
180
- return self._cached_scenario
181
-
182
- def invalidate_scenario_cache(self) -> None:
183
- """Invalidate cached scenario configuration.
184
-
185
- Call this when scenarios are updated at runtime to force
186
- recalculation on the next packet.
187
- """
188
- self._cached_scenario = None
189
-
190
- def _create_response_header(
191
- self, source: int, sequence: int, pkt_type: int, payload_size: int
192
- ) -> LifxHeader:
193
- """Create response header using pre-allocated template (performance).
194
-
195
- This method uses a pre-allocated template and creates a shallow copy,
196
- then updates the fields. This avoids full __init__ and __post_init__
197
- overhead while ensuring each response gets its own header object,
198
- providing ~10% improvement in response generation.
199
-
200
- Args:
201
- source: Source identifier from request
202
- sequence: Sequence number from request
203
- pkt_type: Packet type for response
204
- payload_size: Size of packed payload in bytes
205
-
206
- Returns:
207
- Configured LifxHeader ready to use
208
- """
209
- # Shallow copy of template is faster than full construction with validation
210
- header = copy.copy(self._response_header_template)
211
- # Update fields for this specific response
212
- header.source = source
213
- header.sequence = sequence
214
- header.pkt_type = pkt_type
215
- header.size = LIFX_HEADER_SIZE + payload_size
216
- return header
217
-
218
- def process_packet(
219
- self, header: LifxHeader, packet: Any | None
220
- ) -> list[tuple[LifxHeader, Any]]:
221
- """Process incoming packet and return response packets"""
222
- responses = []
223
-
224
- # Get resolved scenario configuration (cached for performance)
225
- scenario = self._get_resolved_scenario()
226
-
227
- # Check if packet should be dropped (with probabilistic drops)
228
- if not self.scenario_manager.should_respond(header.pkt_type, scenario):
229
- logger.info("Dropping packet type %s per scenario", header.pkt_type)
230
- return responses
231
-
232
- # Update uptime
233
- self.state.uptime_ns = self.get_uptime_ns()
234
-
235
- # Handle acknowledgment (packet type 45, no payload)
236
- if header.ack_required:
237
- ack_packet = Device.Acknowledgement()
238
- ack_payload = ack_packet.pack()
239
- ack_header = self._create_response_header(
240
- header.source,
241
- header.sequence,
242
- ack_packet.PKT_TYPE,
243
- len(ack_payload),
244
- )
245
- # Store header, packet, and pre-packed payload
246
- # (consistent with response format)
247
- responses.append((ack_header, ack_packet, ack_payload))
248
-
249
- # Handle specific packet types - handlers always return list
250
- response_packets = self._handle_packet_type(header, packet)
251
- # Handlers now always return list (empty if no response)
252
- for resp_packet in response_packets:
253
- # Cache packed payload to avoid double packing (performance optimization)
254
- resp_payload = resp_packet.pack()
255
- resp_header = self._create_response_header(
256
- header.source,
257
- header.sequence,
258
- resp_packet.PKT_TYPE,
259
- len(resp_payload),
260
- )
261
- # Store both header and pre-packed payload for error scenario processing
262
- responses.append((resp_header, resp_packet, resp_payload))
263
-
264
- # Apply error scenarios to responses
265
- modified_responses = []
266
- for resp_header, resp_packet, resp_payload in responses:
267
- # Check if we should send malformed packet (truncate payload)
268
- if resp_header.pkt_type in scenario.malformed_packets:
269
- # For malformed packets, truncate the pre-packed payload
270
- truncated_len = len(resp_payload) // 2
271
- resp_payload_modified = resp_payload[:truncated_len]
272
- resp_header.size = LIFX_HEADER_SIZE + truncated_len + 10 # Wrong size
273
- # Convert back to bytes for malformed case
274
- modified_responses.append((resp_header, resp_payload_modified))
275
- logger.info(
276
- "Sending malformed packet type %s (truncated)", resp_header.pkt_type
277
- )
278
- continue
279
-
280
- # Check if we should send invalid field values
281
- if resp_header.pkt_type in scenario.invalid_field_values:
282
- # Corrupt the pre-packed payload
283
- resp_payload_modified = b"\xff" * len(resp_payload)
284
- modified_responses.append((resp_header, resp_payload_modified))
285
- pkt_type = resp_header.pkt_type
286
- logger.info("Sending invalid field values for packet type %s", pkt_type)
287
- continue
288
-
289
- # Normal case: use original packet object (will be packed later by server)
290
- modified_responses.append((resp_header, resp_packet))
291
-
292
- return modified_responses
293
-
294
- def _handle_packet_type(self, header: LifxHeader, packet: Any | None) -> list[Any]:
295
- """Handle specific packet types using registered handlers.
296
-
297
- Returns:
298
- List of response packets (empty list if no response)
299
- """
300
- pkt_type = header.pkt_type
301
-
302
- # Update uptime for this packet
303
- self.state.uptime_ns = self.get_uptime_ns()
304
-
305
- # Find handler for this packet type
306
- handler = self.handlers.get_handler(pkt_type)
307
-
308
- if handler:
309
- # Delegate to handler (always returns list now)
310
- response = handler.handle(self.state, packet, header.res_required)
311
-
312
- # Save state if storage is enabled (for SET operations)
313
- if packet and self.storage:
314
- self._save_state()
315
-
316
- return response
317
- else:
318
- # Unknown/unimplemented packet type
319
- from lifx_emulator.protocol.packets import get_packet_class
320
-
321
- packet_class = get_packet_class(pkt_type)
322
- if packet_class:
323
- logger.info(
324
- "Device %s: Received %s (type %s) but no handler registered",
325
- self.state.serial,
326
- packet_class.__qualname__,
327
- pkt_type,
328
- )
329
- else:
330
- serial = self.state.serial
331
- logger.warning(
332
- "Device %s: Received unknown packet type %s", serial, pkt_type
333
- )
334
-
335
- # Check scenario for StateUnhandled response
336
- scenario = self._get_resolved_scenario()
337
- if scenario.send_unhandled:
338
- return [Device.StateUnhandled(unhandled_type=pkt_type)]
339
- return []
@@ -1,256 +0,0 @@
1
- """Device management for LIFX emulator.
2
-
3
- This module provides the DeviceManager class which handles device lifecycle
4
- operations, packet routing, and device lookup. It follows the separation of
5
- concerns principle by extracting domain logic from the network layer.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import logging
11
- from typing import TYPE_CHECKING, Protocol, runtime_checkable
12
-
13
- if TYPE_CHECKING:
14
- from lifx_emulator.devices.device import EmulatedLifxDevice
15
- from lifx_emulator.protocol.header import LifxHeader
16
- from lifx_emulator.repositories import IDeviceRepository
17
- from lifx_emulator.scenarios import HierarchicalScenarioManager
18
-
19
- logger = logging.getLogger(__name__)
20
-
21
-
22
- @runtime_checkable
23
- class IDeviceManager(Protocol):
24
- """Interface for device management operations."""
25
-
26
- def add_device(
27
- self,
28
- device: EmulatedLifxDevice,
29
- scenario_manager: HierarchicalScenarioManager | None = None,
30
- ) -> bool:
31
- """Add a device to the manager.
32
-
33
- Args:
34
- device: The device to add
35
- scenario_manager: Optional scenario manager to share with the device
36
-
37
- Returns:
38
- True if added, False if device with same serial already exists
39
- """
40
- ...
41
-
42
- def remove_device(self, serial: str, storage=None) -> bool:
43
- """Remove a device from the manager.
44
-
45
- Args:
46
- serial: Serial number of device to remove (12 hex chars)
47
- storage: Optional storage backend to delete persistent state
48
-
49
- Returns:
50
- True if removed, False if device not found
51
- """
52
- ...
53
-
54
- def remove_all_devices(self, delete_storage: bool = False, storage=None) -> int:
55
- """Remove all devices from the manager.
56
-
57
- Args:
58
- delete_storage: If True, also delete persistent storage files
59
- storage: Storage backend to use for deletion
60
-
61
- Returns:
62
- Number of devices removed
63
- """
64
- ...
65
-
66
- def get_device(self, serial: str) -> EmulatedLifxDevice | None:
67
- """Get a device by serial number.
68
-
69
- Args:
70
- serial: Serial number (12 hex chars)
71
-
72
- Returns:
73
- Device if found, None otherwise
74
- """
75
- ...
76
-
77
- def get_all_devices(self) -> list[EmulatedLifxDevice]:
78
- """Get all devices.
79
-
80
- Returns:
81
- List of all devices
82
- """
83
- ...
84
-
85
- def count_devices(self) -> int:
86
- """Get the number of devices.
87
-
88
- Returns:
89
- Number of devices in the manager
90
- """
91
- ...
92
-
93
- def resolve_target_devices(self, header: LifxHeader) -> list[EmulatedLifxDevice]:
94
- """Resolve which devices should handle a packet based on the header.
95
-
96
- Args:
97
- header: Parsed LIFX header containing target information
98
-
99
- Returns:
100
- List of devices that should process this packet
101
- """
102
- ...
103
-
104
- def invalidate_all_scenario_caches(self) -> None:
105
- """Invalidate scenario cache for all devices.
106
-
107
- This should be called when scenario configuration changes to ensure
108
- devices reload their scenario settings from the scenario manager.
109
- """
110
- ...
111
-
112
-
113
- class DeviceManager:
114
- """Manages device lifecycle, routing, and lookup operations.
115
-
116
- This class extracts device management logic from EmulatedLifxServer,
117
- providing a clean separation between domain logic and network I/O.
118
- It mirrors the architecture of HierarchicalScenarioManager.
119
- """
120
-
121
- def __init__(self, device_repository: IDeviceRepository):
122
- """Initialize the device manager.
123
-
124
- Args:
125
- device_repository: Repository for device storage and retrieval
126
- """
127
- self._device_repository = device_repository
128
-
129
- def add_device(
130
- self,
131
- device: EmulatedLifxDevice,
132
- scenario_manager: HierarchicalScenarioManager | None = None,
133
- ) -> bool:
134
- """Add a device to the manager.
135
-
136
- Args:
137
- device: The device to add
138
- scenario_manager: Optional scenario manager to share with the device
139
-
140
- Returns:
141
- True if added, False if device with same serial already exists
142
- """
143
- # If device is using HierarchicalScenarioManager, share the provided manager
144
- if scenario_manager is not None:
145
- from lifx_emulator.scenarios import HierarchicalScenarioManager
146
-
147
- if isinstance(device.scenario_manager, HierarchicalScenarioManager):
148
- device.scenario_manager = scenario_manager
149
- device.invalidate_scenario_cache()
150
-
151
- success = self._device_repository.add(device)
152
- if success:
153
- serial = device.state.serial
154
- logger.info("Added device: %s (product=%s)", serial, device.state.product)
155
- return success
156
-
157
- def remove_device(self, serial: str, storage=None) -> bool:
158
- """Remove a device from the manager.
159
-
160
- Args:
161
- serial: Serial number of device to remove (12 hex chars)
162
- storage: Optional storage backend to delete persistent state
163
-
164
- Returns:
165
- True if removed, False if device not found
166
- """
167
- success = self._device_repository.remove(serial)
168
- if success:
169
- logger.info("Removed device: %s", serial)
170
-
171
- # Delete persistent storage if enabled
172
- if storage:
173
- storage.delete_device_state(serial)
174
-
175
- return success
176
-
177
- def remove_all_devices(self, delete_storage: bool = False, storage=None) -> int:
178
- """Remove all devices from the manager.
179
-
180
- Args:
181
- delete_storage: If True, also delete persistent storage files
182
- storage: Storage backend to use for deletion
183
-
184
- Returns:
185
- Number of devices removed
186
- """
187
- # Clear all devices from repository
188
- device_count = self._device_repository.clear()
189
- logger.info("Removed all %s device(s)", device_count)
190
-
191
- # Delete persistent storage if requested
192
- if delete_storage and storage:
193
- deleted = storage.delete_all_device_states()
194
- logger.info("Deleted %s device state(s) from persistent storage", deleted)
195
-
196
- return device_count
197
-
198
- def get_device(self, serial: str) -> EmulatedLifxDevice | None:
199
- """Get a device by serial number.
200
-
201
- Args:
202
- serial: Serial number (12 hex chars)
203
-
204
- Returns:
205
- Device if found, None otherwise
206
- """
207
- return self._device_repository.get(serial)
208
-
209
- def get_all_devices(self) -> list[EmulatedLifxDevice]:
210
- """Get all devices.
211
-
212
- Returns:
213
- List of all devices
214
- """
215
- return self._device_repository.get_all()
216
-
217
- def count_devices(self) -> int:
218
- """Get the number of devices.
219
-
220
- Returns:
221
- Number of devices in the manager
222
- """
223
- return self._device_repository.count()
224
-
225
- def resolve_target_devices(self, header: LifxHeader) -> list[EmulatedLifxDevice]:
226
- """Resolve which devices should handle a packet based on the header.
227
-
228
- Args:
229
- header: Parsed LIFX header containing target information
230
-
231
- Returns:
232
- List of devices that should process this packet
233
- """
234
- target_devices = []
235
-
236
- if header.tagged or header.target == b"\x00" * 8:
237
- # Broadcast to all devices
238
- target_devices = self._device_repository.get_all()
239
- else:
240
- # Specific device - convert target bytes to serial string
241
- # Target is 8 bytes: 6-byte MAC + 2 null bytes
242
- target_serial = header.target[:6].hex()
243
- device = self._device_repository.get(target_serial)
244
- if device:
245
- target_devices = [device]
246
-
247
- return target_devices
248
-
249
- def invalidate_all_scenario_caches(self) -> None:
250
- """Invalidate scenario cache for all devices.
251
-
252
- This should be called when scenario configuration changes to ensure
253
- devices reload their scenario settings from the scenario manager.
254
- """
255
- for device in self._device_repository.get_all():
256
- device.invalidate_scenario_cache()