lifx-emulator 1.0.2__py3-none-any.whl → 2.1.0__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.
- lifx_emulator/__init__.py +1 -1
- lifx_emulator/__main__.py +26 -51
- lifx_emulator/api/__init__.py +18 -0
- lifx_emulator/api/app.py +154 -0
- lifx_emulator/api/mappers/__init__.py +5 -0
- lifx_emulator/api/mappers/device_mapper.py +114 -0
- lifx_emulator/api/models.py +133 -0
- lifx_emulator/api/routers/__init__.py +11 -0
- lifx_emulator/api/routers/devices.py +130 -0
- lifx_emulator/api/routers/monitoring.py +52 -0
- lifx_emulator/api/routers/scenarios.py +247 -0
- lifx_emulator/api/services/__init__.py +8 -0
- lifx_emulator/api/services/device_service.py +198 -0
- lifx_emulator/{api.py → api/templates/dashboard.html} +0 -942
- lifx_emulator/devices/__init__.py +37 -0
- lifx_emulator/devices/device.py +333 -0
- lifx_emulator/devices/manager.py +256 -0
- lifx_emulator/{async_storage.py → devices/persistence.py} +3 -3
- lifx_emulator/{state_restorer.py → devices/state_restorer.py} +2 -2
- lifx_emulator/devices/states.py +346 -0
- lifx_emulator/factories/__init__.py +37 -0
- lifx_emulator/factories/builder.py +371 -0
- lifx_emulator/factories/default_config.py +158 -0
- lifx_emulator/factories/factory.py +221 -0
- lifx_emulator/factories/firmware_config.py +59 -0
- lifx_emulator/factories/serial_generator.py +82 -0
- lifx_emulator/handlers/base.py +1 -1
- lifx_emulator/handlers/device_handlers.py +10 -28
- lifx_emulator/handlers/light_handlers.py +5 -9
- lifx_emulator/handlers/multizone_handlers.py +1 -1
- lifx_emulator/handlers/tile_handlers.py +31 -11
- lifx_emulator/products/generator.py +389 -170
- lifx_emulator/products/registry.py +52 -40
- lifx_emulator/products/specs.py +12 -13
- lifx_emulator/protocol/base.py +175 -63
- lifx_emulator/protocol/generator.py +18 -5
- lifx_emulator/protocol/packets.py +7 -7
- lifx_emulator/protocol/protocol_types.py +35 -62
- lifx_emulator/repositories/__init__.py +22 -0
- lifx_emulator/repositories/device_repository.py +155 -0
- lifx_emulator/repositories/storage_backend.py +107 -0
- lifx_emulator/scenarios/__init__.py +22 -0
- lifx_emulator/{scenario_manager.py → scenarios/manager.py} +11 -91
- lifx_emulator/scenarios/models.py +112 -0
- lifx_emulator/{scenario_persistence.py → scenarios/persistence.py} +82 -47
- lifx_emulator/server.py +42 -66
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/METADATA +1 -1
- lifx_emulator-2.1.0.dist-info/RECORD +62 -0
- lifx_emulator/device.py +0 -750
- lifx_emulator/device_states.py +0 -114
- lifx_emulator/factories.py +0 -380
- lifx_emulator/storage_protocol.py +0 -100
- lifx_emulator-1.0.2.dist-info/RECORD +0 -40
- /lifx_emulator/{observers.py → devices/observers.py} +0 -0
- /lifx_emulator/{state_serializer.py → devices/state_serializer.py} +0 -0
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/WHEEL +0 -0
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/entry_points.txt +0 -0
- {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,46 +1,75 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Async persistent storage for scenario configurations.
|
|
2
2
|
|
|
3
|
-
This module provides JSON serialization and deserialization for scenarios,
|
|
4
|
-
allowing them to persist across emulator restarts.
|
|
3
|
+
This module provides async JSON serialization and deserialization for scenarios,
|
|
4
|
+
allowing them to persist across emulator restarts without blocking the event loop.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
7
10
|
import json
|
|
8
11
|
import logging
|
|
12
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
9
13
|
from pathlib import Path
|
|
10
14
|
from typing import Any
|
|
11
15
|
|
|
12
|
-
from lifx_emulator.
|
|
16
|
+
from lifx_emulator.scenarios.manager import HierarchicalScenarioManager, ScenarioConfig
|
|
13
17
|
|
|
14
18
|
logger = logging.getLogger(__name__)
|
|
15
19
|
|
|
20
|
+
DEFAULT_STORAGE_DIR = Path.home() / ".lifx-emulator"
|
|
21
|
+
|
|
16
22
|
|
|
17
|
-
class
|
|
18
|
-
"""
|
|
23
|
+
class ScenarioPersistenceAsyncFile:
|
|
24
|
+
"""Async persistent storage for scenario configurations.
|
|
19
25
|
|
|
26
|
+
Non-blocking asynchronous I/O for scenario persistence.
|
|
20
27
|
Scenarios are stored in JSON format at ~/.lifx-emulator/scenarios.json
|
|
21
28
|
with separate sections for each scope level.
|
|
29
|
+
|
|
30
|
+
Features:
|
|
31
|
+
- Async I/O operations (no event loop blocking)
|
|
32
|
+
- Executor-based I/O for file operations
|
|
33
|
+
- Atomic writes (write to temp file, then rename)
|
|
34
|
+
- Graceful error handling and recovery
|
|
22
35
|
"""
|
|
23
36
|
|
|
24
|
-
def __init__(self, storage_path: Path | None = None):
|
|
25
|
-
"""Initialize scenario persistence.
|
|
37
|
+
def __init__(self, storage_path: Path | str | None = None):
|
|
38
|
+
"""Initialize async scenario persistence.
|
|
26
39
|
|
|
27
40
|
Args:
|
|
28
41
|
storage_path: Directory to store scenarios.json
|
|
29
42
|
Defaults to ~/.lifx-emulator
|
|
30
43
|
"""
|
|
31
44
|
if storage_path is None:
|
|
32
|
-
storage_path =
|
|
45
|
+
storage_path = DEFAULT_STORAGE_DIR
|
|
33
46
|
|
|
34
47
|
self.storage_path = Path(storage_path)
|
|
35
48
|
self.scenario_file = self.storage_path / "scenarios.json"
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
# Single-thread executor for serialized I/O operations
|
|
51
|
+
self.executor = ThreadPoolExecutor(
|
|
52
|
+
max_workers=1, thread_name_prefix="scenario-io"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
logger.debug("Async scenario storage initialized at %s", self.storage_path)
|
|
56
|
+
|
|
57
|
+
async def load(self) -> HierarchicalScenarioManager:
|
|
58
|
+
"""Load scenarios from disk (async).
|
|
39
59
|
|
|
40
60
|
Returns:
|
|
41
61
|
HierarchicalScenarioManager with loaded scenarios.
|
|
42
62
|
If file doesn't exist, returns empty manager.
|
|
43
63
|
"""
|
|
64
|
+
loop = asyncio.get_running_loop()
|
|
65
|
+
return await loop.run_in_executor(self.executor, self._sync_load)
|
|
66
|
+
|
|
67
|
+
def _sync_load(self) -> HierarchicalScenarioManager:
|
|
68
|
+
"""Synchronous load operation (runs in executor).
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
HierarchicalScenarioManager with loaded scenarios
|
|
72
|
+
"""
|
|
44
73
|
manager = HierarchicalScenarioManager()
|
|
45
74
|
|
|
46
75
|
if not self.scenario_file.exists():
|
|
@@ -53,12 +82,14 @@ class ScenarioPersistence:
|
|
|
53
82
|
|
|
54
83
|
# Load global scenario
|
|
55
84
|
if "global" in data and data["global"]:
|
|
56
|
-
manager.global_scenario = ScenarioConfig.
|
|
85
|
+
manager.global_scenario = ScenarioConfig.model_validate(data["global"])
|
|
57
86
|
logger.debug("Loaded global scenario")
|
|
58
87
|
|
|
59
88
|
# Load device-specific scenarios
|
|
60
89
|
for serial, config_data in data.get("devices", {}).items():
|
|
61
|
-
manager.device_scenarios[serial] = ScenarioConfig.
|
|
90
|
+
manager.device_scenarios[serial] = ScenarioConfig.model_validate(
|
|
91
|
+
config_data
|
|
92
|
+
)
|
|
62
93
|
if manager.device_scenarios:
|
|
63
94
|
logger.debug(
|
|
64
95
|
"Loaded %s device scenario(s)", len(manager.device_scenarios)
|
|
@@ -66,7 +97,7 @@ class ScenarioPersistence:
|
|
|
66
97
|
|
|
67
98
|
# Load type-specific scenarios
|
|
68
99
|
for device_type, config_data in data.get("types", {}).items():
|
|
69
|
-
manager.type_scenarios[device_type] = ScenarioConfig.
|
|
100
|
+
manager.type_scenarios[device_type] = ScenarioConfig.model_validate(
|
|
70
101
|
config_data
|
|
71
102
|
)
|
|
72
103
|
if manager.type_scenarios:
|
|
@@ -74,7 +105,7 @@ class ScenarioPersistence:
|
|
|
74
105
|
|
|
75
106
|
# Load location-specific scenarios
|
|
76
107
|
for location, config_data in data.get("locations", {}).items():
|
|
77
|
-
manager.location_scenarios[location] = ScenarioConfig.
|
|
108
|
+
manager.location_scenarios[location] = ScenarioConfig.model_validate(
|
|
78
109
|
config_data
|
|
79
110
|
)
|
|
80
111
|
if manager.location_scenarios:
|
|
@@ -84,7 +115,9 @@ class ScenarioPersistence:
|
|
|
84
115
|
|
|
85
116
|
# Load group-specific scenarios
|
|
86
117
|
for group, config_data in data.get("groups", {}).items():
|
|
87
|
-
manager.group_scenarios[group] = ScenarioConfig.
|
|
118
|
+
manager.group_scenarios[group] = ScenarioConfig.model_validate(
|
|
119
|
+
config_data
|
|
120
|
+
)
|
|
88
121
|
if manager.group_scenarios:
|
|
89
122
|
logger.debug(
|
|
90
123
|
"Loaded %s group scenario(s)", len(manager.group_scenarios)
|
|
@@ -100,8 +133,17 @@ class ScenarioPersistence:
|
|
|
100
133
|
logger.error("Failed to load scenarios from %s: %s", self.scenario_file, e)
|
|
101
134
|
return manager
|
|
102
135
|
|
|
103
|
-
def save(self, manager: HierarchicalScenarioManager) -> None:
|
|
104
|
-
"""Save scenarios to disk.
|
|
136
|
+
async def save(self, manager: HierarchicalScenarioManager) -> None:
|
|
137
|
+
"""Save scenarios to disk (async).
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
manager: HierarchicalScenarioManager to save
|
|
141
|
+
"""
|
|
142
|
+
loop = asyncio.get_running_loop()
|
|
143
|
+
await loop.run_in_executor(self.executor, self._sync_save, manager)
|
|
144
|
+
|
|
145
|
+
def _sync_save(self, manager: HierarchicalScenarioManager) -> None:
|
|
146
|
+
"""Synchronous save operation (runs in executor).
|
|
105
147
|
|
|
106
148
|
Args:
|
|
107
149
|
manager: HierarchicalScenarioManager to save
|
|
@@ -156,8 +198,17 @@ class ScenarioPersistence:
|
|
|
156
198
|
logger.error("Failed to save scenarios to %s: %s", self.scenario_file, e)
|
|
157
199
|
raise
|
|
158
200
|
|
|
159
|
-
def delete(self) -> bool:
|
|
160
|
-
"""Delete the scenario file.
|
|
201
|
+
async def delete(self) -> bool:
|
|
202
|
+
"""Delete the scenario file (async).
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
True if file was deleted, False if it didn't exist
|
|
206
|
+
"""
|
|
207
|
+
loop = asyncio.get_running_loop()
|
|
208
|
+
return await loop.run_in_executor(self.executor, self._sync_delete)
|
|
209
|
+
|
|
210
|
+
def _sync_delete(self) -> bool:
|
|
211
|
+
"""Synchronous delete operation (runs in executor).
|
|
161
212
|
|
|
162
213
|
Returns:
|
|
163
214
|
True if file was deleted, False if it didn't exist
|
|
@@ -172,35 +223,19 @@ class ScenarioPersistence:
|
|
|
172
223
|
raise
|
|
173
224
|
return False
|
|
174
225
|
|
|
226
|
+
async def shutdown(self) -> None:
|
|
227
|
+
"""Gracefully shutdown executor.
|
|
175
228
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
JSON only supports string keys, so we convert them back to ints.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
data: Dictionary with string keys
|
|
183
|
-
|
|
184
|
-
Returns:
|
|
185
|
-
Dictionary with integer keys
|
|
186
|
-
"""
|
|
187
|
-
if not data:
|
|
188
|
-
return {}
|
|
189
|
-
return {int(k): v for k, v in data.items()}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
# Monkey-patch ScenarioConfig.from_dict to handle string keys
|
|
193
|
-
_original_from_dict = ScenarioConfig.from_dict
|
|
229
|
+
This should be called before the application exits.
|
|
230
|
+
"""
|
|
231
|
+
logger.info("Shutting down async scenario storage...")
|
|
194
232
|
|
|
233
|
+
# Shutdown executor (non-blocking to avoid hanging on Windows)
|
|
234
|
+
loop = asyncio.get_running_loop()
|
|
235
|
+
await loop.run_in_executor(None, self.executor.shutdown, True)
|
|
195
236
|
|
|
196
|
-
|
|
197
|
-
def _from_dict_with_conversion(cls, data: dict[str, Any]) -> ScenarioConfig:
|
|
198
|
-
"""Create from dictionary with int key conversion."""
|
|
199
|
-
# Convert response_delays string keys to ints
|
|
200
|
-
if "response_delays" in data and data["response_delays"]:
|
|
201
|
-
data = data.copy()
|
|
202
|
-
data["response_delays"] = _deserialize_response_delays(data["response_delays"])
|
|
203
|
-
return _original_from_dict(data)
|
|
237
|
+
logger.info("Async scenario storage shutdown complete")
|
|
204
238
|
|
|
205
239
|
|
|
206
|
-
|
|
240
|
+
# Note: Pydantic's field_validators in ScenarioConfig handle string-to-int
|
|
241
|
+
# key conversion automatically, so no additional deserialization logic is needed.
|
lifx_emulator/server.py
CHANGED
|
@@ -9,17 +9,18 @@ from collections import defaultdict
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from lifx_emulator.constants import LIFX_HEADER_SIZE, LIFX_UDP_PORT
|
|
12
|
-
from lifx_emulator.
|
|
13
|
-
from lifx_emulator.observers import (
|
|
12
|
+
from lifx_emulator.devices import (
|
|
14
13
|
ActivityLogger,
|
|
15
14
|
ActivityObserver,
|
|
15
|
+
EmulatedLifxDevice,
|
|
16
|
+
IDeviceManager,
|
|
16
17
|
NullObserver,
|
|
17
18
|
PacketEvent,
|
|
18
19
|
)
|
|
19
20
|
from lifx_emulator.protocol.header import LifxHeader
|
|
20
21
|
from lifx_emulator.protocol.packets import get_packet_class
|
|
21
|
-
from lifx_emulator.
|
|
22
|
-
from lifx_emulator.
|
|
22
|
+
from lifx_emulator.repositories import IScenarioStorageBackend
|
|
23
|
+
from lifx_emulator.scenarios import HierarchicalScenarioManager
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
25
26
|
|
|
@@ -91,6 +92,7 @@ class EmulatedLifxServer:
|
|
|
91
92
|
def __init__(
|
|
92
93
|
self,
|
|
93
94
|
devices: list[EmulatedLifxDevice],
|
|
95
|
+
device_manager: IDeviceManager,
|
|
94
96
|
bind_address: str = "127.0.0.1",
|
|
95
97
|
port: int = LIFX_UDP_PORT,
|
|
96
98
|
track_activity: bool = True,
|
|
@@ -98,30 +100,35 @@ class EmulatedLifxServer:
|
|
|
98
100
|
activity_observer: ActivityObserver | None = None,
|
|
99
101
|
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
100
102
|
persist_scenarios: bool = False,
|
|
103
|
+
scenario_storage: IScenarioStorageBackend | None = None,
|
|
101
104
|
):
|
|
102
|
-
|
|
105
|
+
# Device manager (required dependency injection)
|
|
106
|
+
self._device_manager = device_manager
|
|
103
107
|
self.bind_address = bind_address
|
|
104
108
|
self.port = port
|
|
105
109
|
self.transport = None
|
|
106
110
|
self.storage = storage
|
|
107
111
|
|
|
108
|
-
# Scenario
|
|
109
|
-
self.scenario_persistence:
|
|
112
|
+
# Scenario storage backend (optional - only needed for persistence)
|
|
113
|
+
self.scenario_persistence: IScenarioStorageBackend | None = None
|
|
110
114
|
if persist_scenarios:
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
if scenario_storage is None:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
"scenario_storage is required when persist_scenarios=True"
|
|
118
|
+
)
|
|
113
119
|
if scenario_manager is None:
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"scenario_manager is required when persist_scenarios=True "
|
|
122
|
+
"(must be pre-loaded from storage before server initialization)"
|
|
123
|
+
)
|
|
124
|
+
self.scenario_persistence = scenario_storage
|
|
116
125
|
|
|
117
126
|
# Scenario manager (shared across all devices for runtime updates)
|
|
118
127
|
self.scenario_manager = scenario_manager or HierarchicalScenarioManager()
|
|
119
128
|
|
|
120
|
-
#
|
|
129
|
+
# Add initial devices to the device manager
|
|
121
130
|
for device in devices:
|
|
122
|
-
|
|
123
|
-
device.scenario_manager = self.scenario_manager
|
|
124
|
-
device.invalidate_scenario_cache()
|
|
131
|
+
self._device_manager.add_device(device, self.scenario_manager)
|
|
125
132
|
|
|
126
133
|
# Activity observer - defaults to ActivityLogger if track_activity=True
|
|
127
134
|
if activity_observer is not None:
|
|
@@ -210,7 +217,7 @@ class EmulatedLifxServer:
|
|
|
210
217
|
resp_packet_name = _get_packet_type_name(resp_header.pkt_type)
|
|
211
218
|
resp_fields_str = _format_packet_fields(resp_packet)
|
|
212
219
|
logger.debug(
|
|
213
|
-
"→ TX %s to %s:%s (
|
|
220
|
+
"→ TX %s to %s:%s (target=%s, seq=%s) [%s]",
|
|
214
221
|
resp_packet_name,
|
|
215
222
|
addr[0],
|
|
216
223
|
addr[1],
|
|
@@ -296,7 +303,9 @@ class EmulatedLifxServer:
|
|
|
296
303
|
|
|
297
304
|
# Log received packet with details
|
|
298
305
|
packet_name = _get_packet_type_name(header.pkt_type)
|
|
299
|
-
target_str =
|
|
306
|
+
target_str = (
|
|
307
|
+
"broadcast" if header.tagged else header.target.hex().rstrip("0000")
|
|
308
|
+
)
|
|
300
309
|
fields_str = _format_packet_fields(packet)
|
|
301
310
|
logger.debug(
|
|
302
311
|
"← RX %s from %s:%s (target=%s, seq=%s) [%s]",
|
|
@@ -320,18 +329,8 @@ class EmulatedLifxServer:
|
|
|
320
329
|
)
|
|
321
330
|
)
|
|
322
331
|
|
|
323
|
-
# Determine target devices
|
|
324
|
-
target_devices =
|
|
325
|
-
if header.tagged or header.target == b"\x00" * 8:
|
|
326
|
-
# Broadcast to all devices
|
|
327
|
-
target_devices = list(self.devices.values())
|
|
328
|
-
else:
|
|
329
|
-
# Specific device - convert target bytes to serial string
|
|
330
|
-
# Target is 8 bytes: 6-byte MAC + 2 null bytes
|
|
331
|
-
target_serial = header.target[:6].hex()
|
|
332
|
-
device = self.devices.get(target_serial)
|
|
333
|
-
if device:
|
|
334
|
-
target_devices = [device]
|
|
332
|
+
# Determine target devices using device manager
|
|
333
|
+
target_devices = self._device_manager.resolve_target_devices(header)
|
|
335
334
|
|
|
336
335
|
# Process packet for each target device
|
|
337
336
|
# Use parallel processing for broadcasts to improve scalability
|
|
@@ -361,18 +360,7 @@ class EmulatedLifxServer:
|
|
|
361
360
|
Returns:
|
|
362
361
|
True if added, False if device with same serial already exists
|
|
363
362
|
"""
|
|
364
|
-
|
|
365
|
-
if serial in self.devices:
|
|
366
|
-
return False
|
|
367
|
-
|
|
368
|
-
# If device is using HierarchicalScenarioManager, share the server's manager
|
|
369
|
-
if isinstance(device.scenario_manager, HierarchicalScenarioManager):
|
|
370
|
-
device.scenario_manager = self.scenario_manager
|
|
371
|
-
device.invalidate_scenario_cache()
|
|
372
|
-
|
|
373
|
-
self.devices[serial] = device
|
|
374
|
-
logger.info("Added device: %s (product=%s)", serial, device.state.product)
|
|
375
|
-
return True
|
|
363
|
+
return self._device_manager.add_device(device, self.scenario_manager)
|
|
376
364
|
|
|
377
365
|
def remove_device(self, serial: str) -> bool:
|
|
378
366
|
"""Remove a device from the server.
|
|
@@ -383,16 +371,7 @@ class EmulatedLifxServer:
|
|
|
383
371
|
Returns:
|
|
384
372
|
True if removed, False if device not found
|
|
385
373
|
"""
|
|
386
|
-
|
|
387
|
-
return False
|
|
388
|
-
self.devices.pop(serial)
|
|
389
|
-
logger.info("Removed device: %s", serial)
|
|
390
|
-
|
|
391
|
-
# Delete persistent storage if enabled
|
|
392
|
-
if self.storage:
|
|
393
|
-
self.storage.delete_device_state(serial)
|
|
394
|
-
|
|
395
|
-
return True
|
|
374
|
+
return self._device_manager.remove_device(serial, self.storage)
|
|
396
375
|
|
|
397
376
|
def remove_all_devices(self, delete_storage: bool = False) -> int:
|
|
398
377
|
"""Remove all devices from the server.
|
|
@@ -403,18 +382,7 @@ class EmulatedLifxServer:
|
|
|
403
382
|
Returns:
|
|
404
383
|
Number of devices removed
|
|
405
384
|
"""
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
# Clear devices dict
|
|
409
|
-
self.devices.clear()
|
|
410
|
-
logger.info("Removed all %s device(s) from server", device_count)
|
|
411
|
-
|
|
412
|
-
# Delete persistent storage if requested
|
|
413
|
-
if delete_storage and self.storage:
|
|
414
|
-
deleted = self.storage.delete_all_device_states()
|
|
415
|
-
logger.info("Deleted %s device state(s) from persistent storage", deleted)
|
|
416
|
-
|
|
417
|
-
return device_count
|
|
385
|
+
return self._device_manager.remove_all_devices(delete_storage, self.storage)
|
|
418
386
|
|
|
419
387
|
def get_device(self, serial: str) -> EmulatedLifxDevice | None:
|
|
420
388
|
"""Get a device by serial number.
|
|
@@ -425,7 +393,7 @@ class EmulatedLifxServer:
|
|
|
425
393
|
Returns:
|
|
426
394
|
Device if found, None otherwise
|
|
427
395
|
"""
|
|
428
|
-
return self.
|
|
396
|
+
return self._device_manager.get_device(serial)
|
|
429
397
|
|
|
430
398
|
def get_all_devices(self) -> list[EmulatedLifxDevice]:
|
|
431
399
|
"""Get all devices.
|
|
@@ -433,7 +401,15 @@ class EmulatedLifxServer:
|
|
|
433
401
|
Returns:
|
|
434
402
|
List of all devices
|
|
435
403
|
"""
|
|
436
|
-
return
|
|
404
|
+
return self._device_manager.get_all_devices()
|
|
405
|
+
|
|
406
|
+
def invalidate_all_scenario_caches(self) -> None:
|
|
407
|
+
"""Invalidate scenario cache for all devices.
|
|
408
|
+
|
|
409
|
+
This should be called when scenario configuration changes to ensure
|
|
410
|
+
devices reload their scenario settings from the scenario manager.
|
|
411
|
+
"""
|
|
412
|
+
self._device_manager.invalidate_all_scenario_caches()
|
|
437
413
|
|
|
438
414
|
def get_stats(self) -> dict[str, Any]:
|
|
439
415
|
"""Get server statistics.
|
|
@@ -445,7 +421,7 @@ class EmulatedLifxServer:
|
|
|
445
421
|
return {
|
|
446
422
|
"uptime_seconds": uptime,
|
|
447
423
|
"start_time": self.start_time,
|
|
448
|
-
"device_count":
|
|
424
|
+
"device_count": self._device_manager.count_devices(),
|
|
449
425
|
"packets_received": self.packets_received,
|
|
450
426
|
"packets_sent": self.packets_sent,
|
|
451
427
|
"packets_received_by_type": dict(self.packets_received_by_type),
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
lifx_emulator/__init__.py,sha256=vjhtpAQRSsUZtaUGCQKbmPALvwZ_BF8Mko8w6jzVqBw,819
|
|
2
|
+
lifx_emulator/__main__.py,sha256=zaul9OQhN5csqOqxGWXkVrlurfo2R_-YvM6URk4QAME,21680
|
|
3
|
+
lifx_emulator/constants.py,sha256=DFZkUsdewE-x_3MgO28tMGkjUCWPeYc3xLj_EXViGOw,1032
|
|
4
|
+
lifx_emulator/server.py,sha256=r2JYFcpZIqqhue-Nfq7FbN0KfC3XDf3XDb6b43DsiCk,16438
|
|
5
|
+
lifx_emulator/api/__init__.py,sha256=FoEPw_In5-H_BDQ-XIIONvgj-UqIDVtejIEVRv9qmV8,647
|
|
6
|
+
lifx_emulator/api/app.py,sha256=IxK8sC7MgdtkoLz8iXcEt02nPDaVgdKJgEiGnzTs-YE,4880
|
|
7
|
+
lifx_emulator/api/models.py,sha256=eBx80Ece_4Wv6aqxb1CsZEob9CF0WmR9oJGz3hh14x8,3973
|
|
8
|
+
lifx_emulator/api/mappers/__init__.py,sha256=ZPCOQR9odcwn0C58AjFW6RvBXe5gOll_QS5lAabgorQ,152
|
|
9
|
+
lifx_emulator/api/mappers/device_mapper.py,sha256=EGOpdao9ZS-vT4T8IoV-AoN5WucTnqpQO92dYizo3vw,4151
|
|
10
|
+
lifx_emulator/api/routers/__init__.py,sha256=kbMefnuXrEsYeMA9J4YK_wVs87_XcH7hwkEifR-zgMc,369
|
|
11
|
+
lifx_emulator/api/routers/devices.py,sha256=i0hFxb9-yA3bbNsk1HyDhHfpAB61o5rObH_vC9gDEpk,4210
|
|
12
|
+
lifx_emulator/api/routers/monitoring.py,sha256=qgVBNm6iMESf1W6EE22DvLalMnxkr0pRbGKu_JDDkPw,1456
|
|
13
|
+
lifx_emulator/api/routers/scenarios.py,sha256=0axSQ9r6rByvXLvqRqOU2ma5nTvZgZ0IIzEXdtzoPnM,9743
|
|
14
|
+
lifx_emulator/api/services/__init__.py,sha256=ttjjZfAxbDQC_Ep0LkXjopNiVZOFPsFDSOHhBN98v5s,277
|
|
15
|
+
lifx_emulator/api/services/device_service.py,sha256=r3uFWApC8sVQMCuuzkyjm27K4LDpZnnHmQNgXWX40ok,6294
|
|
16
|
+
lifx_emulator/api/templates/dashboard.html,sha256=YXQ9jrs30DZIxtMWFE4E2HqmsgHQ-NeWTTQxQ-7BfHk,33800
|
|
17
|
+
lifx_emulator/devices/__init__.py,sha256=QlBTPnFErJcSKLvGyeDwemh7xcpjYvB_L5siKsjr3s8,1089
|
|
18
|
+
lifx_emulator/devices/device.py,sha256=LMdg__95n6geG_32j7qp5yl51WNS3ZbCXn-xMfVVikE,13294
|
|
19
|
+
lifx_emulator/devices/manager.py,sha256=XDrT82um5sgNpNihLj5RsNvHqdVI1bK9YY2eBzWIcf0,8162
|
|
20
|
+
lifx_emulator/devices/observers.py,sha256=-KnUgFcKdhlNo7CNVstP-u0wU2W0JAGg055ZPV15Sj0,3874
|
|
21
|
+
lifx_emulator/devices/persistence.py,sha256=9Mhj46-xrweOmyzjORCi2jKIwa8XJWpQ5CgaKcw6U98,10513
|
|
22
|
+
lifx_emulator/devices/state_restorer.py,sha256=eDsRSW-2RviP_0Qlk2DHqMaB-zhV0X1cNQECv2lD1qc,9809
|
|
23
|
+
lifx_emulator/devices/state_serializer.py,sha256=O4Cp3bbGkd4eZf5jzb0MKzWDTgiNhrSGgypmMWaB4dg,5097
|
|
24
|
+
lifx_emulator/devices/states.py,sha256=mVZz7FQeIHLpv2SokmhlQlSBIyVj3GuhGMHBVoFlJqk,10836
|
|
25
|
+
lifx_emulator/factories/__init__.py,sha256=yN8i_Hu_cFEryWZmh0TiOQvWEYFVIApQSs4xeb0EfBk,1170
|
|
26
|
+
lifx_emulator/factories/builder.py,sha256=ZSz5apcorsKpuPsdjFE4VLC1p41jVY8MWs1-nRBOLMk,11996
|
|
27
|
+
lifx_emulator/factories/default_config.py,sha256=FTcxKDfeTmO49GTSki8nxnEIZQzR0Lg0hL_PwHUrkVQ,4828
|
|
28
|
+
lifx_emulator/factories/factory.py,sha256=VQfU5M8zrpFyNHjpGP1q-3bpek9MltBdoAUSvIvt7Bs,7583
|
|
29
|
+
lifx_emulator/factories/firmware_config.py,sha256=AzvPvR4pfwjK1yNsaua1L9V1gLVItUVySjcGrXIWnEw,1932
|
|
30
|
+
lifx_emulator/factories/serial_generator.py,sha256=MbaXoommsj76ho8_ZoKuUDnffDf98YvwQiXZSWsUsEs,2507
|
|
31
|
+
lifx_emulator/handlers/__init__.py,sha256=3Hj1hRo3yL3E7GKwG9TaYh33ymk_N3bRiQ8nvqSQULA,1306
|
|
32
|
+
lifx_emulator/handlers/base.py,sha256=0avCLXY_rNlw16PpJ5JrRCwXNE4uMpBqF3PfSfNJ0b8,1654
|
|
33
|
+
lifx_emulator/handlers/device_handlers.py,sha256=1AmslA4Ut6L7b3SfduDdvnQizTpzUB3KKWBXmp4WYLQ,9462
|
|
34
|
+
lifx_emulator/handlers/light_handlers.py,sha256=Ryz-_fzoVCT6DBkXhW9YCOYJYaMRcBOIguL3HrQXhAw,11471
|
|
35
|
+
lifx_emulator/handlers/multizone_handlers.py,sha256=2dYsitq0KzEaxEAJmz7ixtir1tvFMOAnfkBQqslqbPM,7914
|
|
36
|
+
lifx_emulator/handlers/registry.py,sha256=s1ht4PmPhXhAcwu1hoY4yW39wy3SPJBMY-9Uxd0FWuE,3292
|
|
37
|
+
lifx_emulator/handlers/tile_handlers.py,sha256=-DU4PufPgE7vfvKsZfxP_7vBtI3EtAeBF3-U2-1zyaQ,11294
|
|
38
|
+
lifx_emulator/products/__init__.py,sha256=qcNop_kRYFF3zSjNemzQEgu3jPrIxfyQyLv9GsnaLEI,627
|
|
39
|
+
lifx_emulator/products/generator.py,sha256=NYInVSGyYIxAYMpTihqBtXP06lAYVfbSYe0Wv5Hg9vQ,31758
|
|
40
|
+
lifx_emulator/products/registry.py,sha256=qkm2xgGZo_ds3wAbYplLu4gb0cxhjZXjnCc1V8etpHw,46517
|
|
41
|
+
lifx_emulator/products/specs.py,sha256=pfmQMrQxlCGqORs3MbsH_vmCvxdaDwjVzXUCVZCjFCI,7093
|
|
42
|
+
lifx_emulator/products/specs.yml,sha256=uxzdKFREAHphk8XSPiCHvQE2vwoPfT2m1xy-zC4ZIl4,8552
|
|
43
|
+
lifx_emulator/protocol/__init__.py,sha256=-wjC-wBcb7fxi5I-mJr2Ad8K2YRflJFdLLdobfD-W1Q,56
|
|
44
|
+
lifx_emulator/protocol/base.py,sha256=V6t0baSgIXjrsz2dBuUn_V9xwradSqMxBFJHAUtnfCs,15368
|
|
45
|
+
lifx_emulator/protocol/const.py,sha256=ilhv-KcQpHtKh2MDCaIbMLQAsxKO_uTaxyR63v1W8cc,226
|
|
46
|
+
lifx_emulator/protocol/generator.py,sha256=LUkf-1Z5570Vg5iA1QhDZDWQOrABqmukUgk9qH-IJmg,49524
|
|
47
|
+
lifx_emulator/protocol/header.py,sha256=RXMJ5YZG1jyxl4Mz46ZGJBYX41Jdp7J95BHuY-scYC0,5499
|
|
48
|
+
lifx_emulator/protocol/packets.py,sha256=Yv4O-Uqbj0CR7n04vXhfalJVCmTTvJTWkvZBkcwPx-U,41553
|
|
49
|
+
lifx_emulator/protocol/protocol_types.py,sha256=WX1p4fmFcNJURmEV_B7ubi7fgu-w9loXQ89q8DdbeSA,23970
|
|
50
|
+
lifx_emulator/protocol/serializer.py,sha256=2bZz7TddxaMRO4_6LujRGCS1w7GxD4E3rRk3r-hpEIE,10738
|
|
51
|
+
lifx_emulator/repositories/__init__.py,sha256=x-ncM6T_Q7jNrwhK4a1uAyMrTGHHGeUzPSLC4O-kEUw,645
|
|
52
|
+
lifx_emulator/repositories/device_repository.py,sha256=KsXVg2sg7PGSTsK_PvDYeHHwEPM9Qx2ZZF_ORncBrYQ,3929
|
|
53
|
+
lifx_emulator/repositories/storage_backend.py,sha256=wEgjhnBvAxl6aO1ZGL3ou0dW9P2hBPnK8jEE03sOlL4,3264
|
|
54
|
+
lifx_emulator/scenarios/__init__.py,sha256=CGjudoWvyysvFj2xej11N2cr3mYROGtRb9zVHcOHGrQ,665
|
|
55
|
+
lifx_emulator/scenarios/manager.py,sha256=1esxRdz74UynNk1wb86MGZ2ZFAuMzByuu74nRe3D-Og,11163
|
|
56
|
+
lifx_emulator/scenarios/models.py,sha256=BKS_fGvrbkGe-vK3arZ0w2f9adS1UZhiOoKpu7GENnc,4099
|
|
57
|
+
lifx_emulator/scenarios/persistence.py,sha256=3vjtPNFYfag38tUxuqxkGpWhQ7uBitc1rLroSAuw9N8,8881
|
|
58
|
+
lifx_emulator-2.1.0.dist-info/METADATA,sha256=cen3ovCv4G8WJyDQpy46bpFndEZ1OZH09NFuaoE0-mw,4549
|
|
59
|
+
lifx_emulator-2.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
60
|
+
lifx_emulator-2.1.0.dist-info/entry_points.txt,sha256=R9C_K_tTgt6yXEmhzH4r2Yx2Tu1rLlnYzeG4RFUVzSc,62
|
|
61
|
+
lifx_emulator-2.1.0.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
|
|
62
|
+
lifx_emulator-2.1.0.dist-info/RECORD,,
|