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.
Files changed (58) hide show
  1. lifx_emulator/__init__.py +1 -1
  2. lifx_emulator/__main__.py +26 -51
  3. lifx_emulator/api/__init__.py +18 -0
  4. lifx_emulator/api/app.py +154 -0
  5. lifx_emulator/api/mappers/__init__.py +5 -0
  6. lifx_emulator/api/mappers/device_mapper.py +114 -0
  7. lifx_emulator/api/models.py +133 -0
  8. lifx_emulator/api/routers/__init__.py +11 -0
  9. lifx_emulator/api/routers/devices.py +130 -0
  10. lifx_emulator/api/routers/monitoring.py +52 -0
  11. lifx_emulator/api/routers/scenarios.py +247 -0
  12. lifx_emulator/api/services/__init__.py +8 -0
  13. lifx_emulator/api/services/device_service.py +198 -0
  14. lifx_emulator/{api.py → api/templates/dashboard.html} +0 -942
  15. lifx_emulator/devices/__init__.py +37 -0
  16. lifx_emulator/devices/device.py +333 -0
  17. lifx_emulator/devices/manager.py +256 -0
  18. lifx_emulator/{async_storage.py → devices/persistence.py} +3 -3
  19. lifx_emulator/{state_restorer.py → devices/state_restorer.py} +2 -2
  20. lifx_emulator/devices/states.py +346 -0
  21. lifx_emulator/factories/__init__.py +37 -0
  22. lifx_emulator/factories/builder.py +371 -0
  23. lifx_emulator/factories/default_config.py +158 -0
  24. lifx_emulator/factories/factory.py +221 -0
  25. lifx_emulator/factories/firmware_config.py +59 -0
  26. lifx_emulator/factories/serial_generator.py +82 -0
  27. lifx_emulator/handlers/base.py +1 -1
  28. lifx_emulator/handlers/device_handlers.py +10 -28
  29. lifx_emulator/handlers/light_handlers.py +5 -9
  30. lifx_emulator/handlers/multizone_handlers.py +1 -1
  31. lifx_emulator/handlers/tile_handlers.py +31 -11
  32. lifx_emulator/products/generator.py +389 -170
  33. lifx_emulator/products/registry.py +52 -40
  34. lifx_emulator/products/specs.py +12 -13
  35. lifx_emulator/protocol/base.py +175 -63
  36. lifx_emulator/protocol/generator.py +18 -5
  37. lifx_emulator/protocol/packets.py +7 -7
  38. lifx_emulator/protocol/protocol_types.py +35 -62
  39. lifx_emulator/repositories/__init__.py +22 -0
  40. lifx_emulator/repositories/device_repository.py +155 -0
  41. lifx_emulator/repositories/storage_backend.py +107 -0
  42. lifx_emulator/scenarios/__init__.py +22 -0
  43. lifx_emulator/{scenario_manager.py → scenarios/manager.py} +11 -91
  44. lifx_emulator/scenarios/models.py +112 -0
  45. lifx_emulator/{scenario_persistence.py → scenarios/persistence.py} +82 -47
  46. lifx_emulator/server.py +42 -66
  47. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/METADATA +1 -1
  48. lifx_emulator-2.1.0.dist-info/RECORD +62 -0
  49. lifx_emulator/device.py +0 -750
  50. lifx_emulator/device_states.py +0 -114
  51. lifx_emulator/factories.py +0 -380
  52. lifx_emulator/storage_protocol.py +0 -100
  53. lifx_emulator-1.0.2.dist-info/RECORD +0 -40
  54. /lifx_emulator/{observers.py → devices/observers.py} +0 -0
  55. /lifx_emulator/{state_serializer.py → devices/state_serializer.py} +0 -0
  56. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/WHEEL +0 -0
  57. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/entry_points.txt +0 -0
  58. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,46 +1,75 @@
1
- """Persistence layer for scenario configurations.
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.scenario_manager import HierarchicalScenarioManager, ScenarioConfig
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 ScenarioPersistence:
18
- """Handles scenario persistence to disk.
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 = Path.home() / ".lifx-emulator"
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
- def load(self) -> HierarchicalScenarioManager:
38
- """Load scenarios from disk.
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.from_dict(data["global"])
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.from_dict(config_data)
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.from_dict(
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.from_dict(
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.from_dict(config_data)
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
- def _deserialize_response_delays(data: dict[str, Any]) -> dict[int, float]:
177
- """Convert string keys back to integers for response_delays.
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
- @classmethod
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
- ScenarioConfig.from_dict = _from_dict_with_conversion # type: ignore
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.device import EmulatedLifxDevice
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.scenario_manager import HierarchicalScenarioManager
22
- from lifx_emulator.scenario_persistence import ScenarioPersistence
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
- self.devices = {dev.state.serial: dev for dev in devices}
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 persistence (optional)
109
- self.scenario_persistence: ScenarioPersistence | None = None
112
+ # Scenario storage backend (optional - only needed for persistence)
113
+ self.scenario_persistence: IScenarioStorageBackend | None = None
110
114
  if persist_scenarios:
111
- self.scenario_persistence = ScenarioPersistence()
112
- # Load scenarios from disk if available
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
- scenario_manager = self.scenario_persistence.load()
115
- logger.info("Loaded scenarios from persistent storage")
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
- # Share scenario manager with all initial devices
129
+ # Add initial devices to the device manager
121
130
  for device in devices:
122
- if isinstance(device.scenario_manager, HierarchicalScenarioManager):
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 (device=%s, seq=%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 = "broadcast" if header.tagged else header.target.hex()
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
- serial = device.state.serial
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
- if serial not in self.devices:
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
- device_count = len(self.devices)
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.devices.get(serial)
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 list(self.devices.values())
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": len(self.devices),
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),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-emulator
3
- Version: 1.0.2
3
+ Version: 2.1.0
4
4
  Summary: LIFX Emulator for testing LIFX LAN protocol libraries
5
5
  Author-email: Avi Miller <me@dje.li>
6
6
  Maintainer-email: Avi Miller <me@dje.li>
@@ -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,,