lifx-emulator 1.0.2__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 -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 +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 +389 -170
  33. lifx_emulator/products/registry.py +52 -40
  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.2.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.2.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.2.dist-info → lifx_emulator-2.0.0.dist-info}/WHEEL +0 -0
  56. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/entry_points.txt +0 -0
  57. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.0.0.dist-info}/licenses/LICENSE +0 -0
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.2
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,,