lifx-emulator 1.0.1__py3-none-any.whl → 2.0.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 (57) hide show
  1. lifx_emulator/__init__.py +1 -1
  2. lifx_emulator/__main__.py +26 -49
  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 +333 -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 +1 -1
  32. lifx_emulator/products/generator.py +406 -175
  33. lifx_emulator/products/registry.py +115 -65
  34. lifx_emulator/products/specs.py +12 -13
  35. lifx_emulator/protocol/base.py +115 -61
  36. lifx_emulator/protocol/generator.py +18 -5
  37. lifx_emulator/protocol/packets.py +7 -7
  38. lifx_emulator/repositories/__init__.py +22 -0
  39. lifx_emulator/repositories/device_repository.py +155 -0
  40. lifx_emulator/repositories/storage_backend.py +107 -0
  41. lifx_emulator/scenarios/__init__.py +22 -0
  42. lifx_emulator/{scenario_manager.py → scenarios/manager.py} +11 -91
  43. lifx_emulator/scenarios/models.py +112 -0
  44. lifx_emulator/{scenario_persistence.py → scenarios/persistence.py} +82 -47
  45. lifx_emulator/server.py +38 -64
  46. {lifx_emulator-1.0.1.dist-info → lifx_emulator-2.0.0.dist-info}/METADATA +1 -1
  47. lifx_emulator-2.0.0.dist-info/RECORD +62 -0
  48. lifx_emulator/device.py +0 -750
  49. lifx_emulator/device_states.py +0 -114
  50. lifx_emulator/factories.py +0 -380
  51. lifx_emulator/storage_protocol.py +0 -100
  52. lifx_emulator-1.0.1.dist-info/RECORD +0 -40
  53. /lifx_emulator/{observers.py → devices/observers.py} +0 -0
  54. /lifx_emulator/{state_serializer.py → devices/state_serializer.py} +0 -0
  55. {lifx_emulator-1.0.1.dist-info → lifx_emulator-2.0.0.dist-info}/WHEEL +0 -0
  56. {lifx_emulator-1.0.1.dist-info → lifx_emulator-2.0.0.dist-info}/entry_points.txt +0 -0
  57. {lifx_emulator-1.0.1.dist-info → lifx_emulator-2.0.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:
@@ -320,18 +327,8 @@ class EmulatedLifxServer:
320
327
  )
321
328
  )
322
329
 
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]
330
+ # Determine target devices using device manager
331
+ target_devices = self._device_manager.resolve_target_devices(header)
335
332
 
336
333
  # Process packet for each target device
337
334
  # Use parallel processing for broadcasts to improve scalability
@@ -361,18 +358,7 @@ class EmulatedLifxServer:
361
358
  Returns:
362
359
  True if added, False if device with same serial already exists
363
360
  """
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
361
+ return self._device_manager.add_device(device, self.scenario_manager)
376
362
 
377
363
  def remove_device(self, serial: str) -> bool:
378
364
  """Remove a device from the server.
@@ -383,16 +369,7 @@ class EmulatedLifxServer:
383
369
  Returns:
384
370
  True if removed, False if device not found
385
371
  """
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
372
+ return self._device_manager.remove_device(serial, self.storage)
396
373
 
397
374
  def remove_all_devices(self, delete_storage: bool = False) -> int:
398
375
  """Remove all devices from the server.
@@ -403,18 +380,7 @@ class EmulatedLifxServer:
403
380
  Returns:
404
381
  Number of devices removed
405
382
  """
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
383
+ return self._device_manager.remove_all_devices(delete_storage, self.storage)
418
384
 
419
385
  def get_device(self, serial: str) -> EmulatedLifxDevice | None:
420
386
  """Get a device by serial number.
@@ -425,7 +391,7 @@ class EmulatedLifxServer:
425
391
  Returns:
426
392
  Device if found, None otherwise
427
393
  """
428
- return self.devices.get(serial)
394
+ return self._device_manager.get_device(serial)
429
395
 
430
396
  def get_all_devices(self) -> list[EmulatedLifxDevice]:
431
397
  """Get all devices.
@@ -433,7 +399,15 @@ class EmulatedLifxServer:
433
399
  Returns:
434
400
  List of all devices
435
401
  """
436
- return list(self.devices.values())
402
+ return self._device_manager.get_all_devices()
403
+
404
+ def invalidate_all_scenario_caches(self) -> None:
405
+ """Invalidate scenario cache for all devices.
406
+
407
+ This should be called when scenario configuration changes to ensure
408
+ devices reload their scenario settings from the scenario manager.
409
+ """
410
+ self._device_manager.invalidate_all_scenario_caches()
437
411
 
438
412
  def get_stats(self) -> dict[str, Any]:
439
413
  """Get server statistics.
@@ -445,7 +419,7 @@ class EmulatedLifxServer:
445
419
  return {
446
420
  "uptime_seconds": uptime,
447
421
  "start_time": self.start_time,
448
- "device_count": len(self.devices),
422
+ "device_count": self._device_manager.count_devices(),
449
423
  "packets_received": self.packets_received,
450
424
  "packets_sent": self.packets_sent,
451
425
  "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.1
3
+ Version: 2.0.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=0bn7oDIlC6TTOJj9ULXLp9rCFAFcW4vM4whor7VTuRU,16391
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=ealrShXAqEeKYnyNclTGgWxV9uDf3VYyw4SbRHe1xEk,10205
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=D23dQVwukfKccryNEFrojMFhubcg4p-onMCXEDRyTlc,10039
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=8DyJBhJi9k5LH4qRe-9P-XBC0iUEH01lGodoADH6Za8,13209
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=2Mccm9717EuTXQYaW44W_yReI4EtnlPp3-WEVASgdGY,24820
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.0.0.dist-info/METADATA,sha256=u29qYpMQ0IbZju5mormUZu6Nye04gpQnxBMWnmSYNiM,4549
59
+ lifx_emulator-2.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
60
+ lifx_emulator-2.0.0.dist-info/entry_points.txt,sha256=R9C_K_tTgt6yXEmhzH4r2Yx2Tu1rLlnYzeG4RFUVzSc,62
61
+ lifx_emulator-2.0.0.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
62
+ lifx_emulator-2.0.0.dist-info/RECORD,,