lifx-emulator 2.4.0__py3-none-any.whl → 3.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 (70) hide show
  1. lifx_emulator-3.1.0.dist-info/METADATA +103 -0
  2. lifx_emulator-3.1.0.dist-info/RECORD +19 -0
  3. {lifx_emulator-2.4.0.dist-info → lifx_emulator-3.1.0.dist-info}/WHEEL +1 -1
  4. lifx_emulator-3.1.0.dist-info/entry_points.txt +2 -0
  5. lifx_emulator_app/__init__.py +10 -0
  6. {lifx_emulator → lifx_emulator_app}/__main__.py +2 -3
  7. {lifx_emulator → lifx_emulator_app}/api/__init__.py +1 -1
  8. {lifx_emulator → lifx_emulator_app}/api/app.py +9 -4
  9. {lifx_emulator → lifx_emulator_app}/api/mappers/__init__.py +1 -1
  10. {lifx_emulator → lifx_emulator_app}/api/mappers/device_mapper.py +1 -1
  11. {lifx_emulator → lifx_emulator_app}/api/models.py +1 -2
  12. lifx_emulator_app/api/routers/__init__.py +11 -0
  13. {lifx_emulator → lifx_emulator_app}/api/routers/devices.py +2 -2
  14. {lifx_emulator → lifx_emulator_app}/api/routers/monitoring.py +1 -1
  15. {lifx_emulator → lifx_emulator_app}/api/routers/scenarios.py +1 -1
  16. lifx_emulator_app/api/services/__init__.py +8 -0
  17. {lifx_emulator → lifx_emulator_app}/api/services/device_service.py +3 -2
  18. lifx_emulator_app/api/static/dashboard.js +588 -0
  19. lifx_emulator_app/api/templates/dashboard.html +357 -0
  20. lifx_emulator/__init__.py +0 -31
  21. lifx_emulator/api/routers/__init__.py +0 -11
  22. lifx_emulator/api/services/__init__.py +0 -8
  23. lifx_emulator/api/templates/dashboard.html +0 -899
  24. lifx_emulator/constants.py +0 -33
  25. lifx_emulator/devices/__init__.py +0 -37
  26. lifx_emulator/devices/device.py +0 -395
  27. lifx_emulator/devices/manager.py +0 -256
  28. lifx_emulator/devices/observers.py +0 -139
  29. lifx_emulator/devices/persistence.py +0 -308
  30. lifx_emulator/devices/state_restorer.py +0 -259
  31. lifx_emulator/devices/state_serializer.py +0 -157
  32. lifx_emulator/devices/states.py +0 -381
  33. lifx_emulator/factories/__init__.py +0 -39
  34. lifx_emulator/factories/builder.py +0 -375
  35. lifx_emulator/factories/default_config.py +0 -158
  36. lifx_emulator/factories/factory.py +0 -252
  37. lifx_emulator/factories/firmware_config.py +0 -77
  38. lifx_emulator/factories/serial_generator.py +0 -82
  39. lifx_emulator/handlers/__init__.py +0 -39
  40. lifx_emulator/handlers/base.py +0 -49
  41. lifx_emulator/handlers/device_handlers.py +0 -322
  42. lifx_emulator/handlers/light_handlers.py +0 -503
  43. lifx_emulator/handlers/multizone_handlers.py +0 -249
  44. lifx_emulator/handlers/registry.py +0 -110
  45. lifx_emulator/handlers/tile_handlers.py +0 -488
  46. lifx_emulator/products/__init__.py +0 -28
  47. lifx_emulator/products/generator.py +0 -1079
  48. lifx_emulator/products/registry.py +0 -1530
  49. lifx_emulator/products/specs.py +0 -284
  50. lifx_emulator/products/specs.yml +0 -386
  51. lifx_emulator/protocol/__init__.py +0 -1
  52. lifx_emulator/protocol/base.py +0 -446
  53. lifx_emulator/protocol/const.py +0 -8
  54. lifx_emulator/protocol/generator.py +0 -1384
  55. lifx_emulator/protocol/header.py +0 -159
  56. lifx_emulator/protocol/packets.py +0 -1351
  57. lifx_emulator/protocol/protocol_types.py +0 -817
  58. lifx_emulator/protocol/serializer.py +0 -379
  59. lifx_emulator/repositories/__init__.py +0 -22
  60. lifx_emulator/repositories/device_repository.py +0 -155
  61. lifx_emulator/repositories/storage_backend.py +0 -107
  62. lifx_emulator/scenarios/__init__.py +0 -22
  63. lifx_emulator/scenarios/manager.py +0 -322
  64. lifx_emulator/scenarios/models.py +0 -112
  65. lifx_emulator/scenarios/persistence.py +0 -241
  66. lifx_emulator/server.py +0 -464
  67. lifx_emulator-2.4.0.dist-info/METADATA +0 -107
  68. lifx_emulator-2.4.0.dist-info/RECORD +0 -62
  69. lifx_emulator-2.4.0.dist-info/entry_points.txt +0 -2
  70. lifx_emulator-2.4.0.dist-info/licenses/LICENSE +0 -35
@@ -1,252 +0,0 @@
1
- """Factory functions for creating emulated LIFX devices."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import TYPE_CHECKING
6
-
7
- from lifx_emulator.devices import EmulatedLifxDevice
8
- from lifx_emulator.factories.builder import DeviceBuilder
9
- from lifx_emulator.products.registry import get_product
10
-
11
- if TYPE_CHECKING:
12
- from lifx_emulator.devices import DevicePersistenceAsyncFile
13
- from lifx_emulator.scenarios import HierarchicalScenarioManager
14
-
15
-
16
- def create_color_light(
17
- serial: str | None = None,
18
- firmware_version: tuple[int, int] | None = None,
19
- storage: DevicePersistenceAsyncFile | None = None,
20
- scenario_manager: HierarchicalScenarioManager | None = None,
21
- ) -> EmulatedLifxDevice:
22
- """Create a regular color light (LIFX Color)"""
23
- return create_device(
24
- 91,
25
- serial=serial,
26
- firmware_version=firmware_version,
27
- storage=storage,
28
- scenario_manager=scenario_manager,
29
- ) # LIFX Color
30
-
31
-
32
- def create_infrared_light(
33
- serial: str | None = None,
34
- firmware_version: tuple[int, int] | None = None,
35
- storage: DevicePersistenceAsyncFile | None = None,
36
- scenario_manager: HierarchicalScenarioManager | None = None,
37
- ) -> EmulatedLifxDevice:
38
- """Create an infrared-enabled light (LIFX A19 Night Vision)"""
39
- return create_device(
40
- 29,
41
- serial=serial,
42
- firmware_version=firmware_version,
43
- storage=storage,
44
- scenario_manager=scenario_manager,
45
- ) # LIFX A19 Night Vision
46
-
47
-
48
- def create_hev_light(
49
- serial: str | None = None,
50
- firmware_version: tuple[int, int] | None = None,
51
- storage: DevicePersistenceAsyncFile | None = None,
52
- scenario_manager: HierarchicalScenarioManager | None = None,
53
- ) -> EmulatedLifxDevice:
54
- """Create an HEV-enabled light (LIFX Clean)"""
55
- return create_device(
56
- 90,
57
- serial=serial,
58
- firmware_version=firmware_version,
59
- storage=storage,
60
- scenario_manager=scenario_manager,
61
- ) # LIFX Clean
62
-
63
-
64
- def create_multizone_light(
65
- serial: str | None = None,
66
- zone_count: int | None = None,
67
- extended_multizone: bool = True,
68
- firmware_version: tuple[int, int] | None = None,
69
- storage: DevicePersistenceAsyncFile | None = None,
70
- scenario_manager: HierarchicalScenarioManager | None = None,
71
- ) -> EmulatedLifxDevice:
72
- """Create a multizone light (LIFX Beam)
73
-
74
- Args:
75
- serial: Optional serial
76
- zone_count: Optional zone count (uses product default if not specified)
77
- extended_multizone: enables support for extended multizone requests
78
- firmware_version: Optional firmware version tuple (major, minor)
79
- storage: Optional storage for persistence
80
- scenario_manager: Optional scenario manager
81
- """
82
- return create_device(
83
- 38,
84
- serial=serial,
85
- zone_count=zone_count,
86
- extended_multizone=extended_multizone,
87
- firmware_version=firmware_version,
88
- storage=storage,
89
- scenario_manager=scenario_manager,
90
- )
91
-
92
-
93
- def create_tile_device(
94
- serial: str | None = None,
95
- tile_count: int | None = None,
96
- tile_width: int | None = None,
97
- tile_height: int | None = None,
98
- firmware_version: tuple[int, int] | None = None,
99
- storage: DevicePersistenceAsyncFile | None = None,
100
- scenario_manager: HierarchicalScenarioManager | None = None,
101
- ) -> EmulatedLifxDevice:
102
- """Create a tile device (LIFX Tile)
103
-
104
- Args:
105
- serial: Optional serial
106
- tile_count: Optional tile count (uses product default)
107
- tile_width: Optional tile width in zones (uses product default)
108
- tile_height: Optional tile height in zones (uses product default)
109
- firmware_version: Optional firmware version tuple (major, minor)
110
- storage: Optional storage for persistence
111
- scenario_manager: Optional scenario manager
112
- """
113
- return create_device(
114
- 55,
115
- serial=serial,
116
- tile_count=tile_count,
117
- tile_width=tile_width,
118
- tile_height=tile_height,
119
- firmware_version=firmware_version,
120
- storage=storage,
121
- scenario_manager=scenario_manager,
122
- ) # LIFX Tile
123
-
124
-
125
- def create_color_temperature_light(
126
- serial: str | None = None,
127
- firmware_version: tuple[int, int] | None = None,
128
- storage: DevicePersistenceAsyncFile | None = None,
129
- scenario_manager: HierarchicalScenarioManager | None = None,
130
- ) -> EmulatedLifxDevice:
131
- """Create a color temperature light (LIFX Mini White to Warm).
132
-
133
- Variable color temperature, no RGB.
134
- """
135
- return create_device(
136
- 50,
137
- serial=serial,
138
- firmware_version=firmware_version,
139
- storage=storage,
140
- scenario_manager=scenario_manager,
141
- ) # LIFX Mini White to Warm
142
-
143
-
144
- def create_switch(
145
- serial: str | None = None,
146
- product_id: int = 70,
147
- firmware_version: tuple[int, int] | None = None,
148
- storage: DevicePersistenceAsyncFile | None = None,
149
- scenario_manager: HierarchicalScenarioManager | None = None,
150
- ) -> EmulatedLifxDevice:
151
- """Create a LIFX Switch device.
152
-
153
- Switches have has_relays and has_buttons capabilities but no lighting.
154
- They respond with StateUnhandled (223) to Light, MultiZone, and Tile packets.
155
-
156
- Args:
157
- serial: Device serial number (auto-generated if None)
158
- product_id: Switch product ID (default: 70 - LIFX Switch)
159
- firmware_version: Optional firmware version (major, minor)
160
- storage: Optional persistence backend
161
- scenario_manager: Optional scenario manager for testing
162
-
163
- Returns:
164
- EmulatedLifxDevice configured as a switch
165
- """
166
- return create_device(
167
- product_id,
168
- serial=serial,
169
- firmware_version=firmware_version,
170
- storage=storage,
171
- scenario_manager=scenario_manager,
172
- )
173
-
174
-
175
- def create_device(
176
- product_id: int,
177
- serial: str | None = None,
178
- zone_count: int | None = None,
179
- extended_multizone: bool | None = None,
180
- tile_count: int | None = None,
181
- tile_width: int | None = None,
182
- tile_height: int | None = None,
183
- firmware_version: tuple[int, int] | None = None,
184
- storage: DevicePersistenceAsyncFile | None = None,
185
- scenario_manager: HierarchicalScenarioManager | None = None,
186
- ) -> EmulatedLifxDevice:
187
- """Create a device for any LIFX product using the product registry.
188
-
189
- This function uses the DeviceBuilder pattern to construct devices with
190
- clean separation of concerns and testable components.
191
-
192
- Args:
193
- product_id: Product ID from the LIFX product registry
194
- serial: Optional serial (auto-generated if not provided)
195
- zone_count: Number of zones for multizone devices (auto-determined)
196
- extended_multizone: Enable extended multizone requests
197
- tile_count: Number of tiles for matrix devices (default: 5)
198
- tile_width: Width of each tile in zones (default: 8)
199
- tile_height: Height of each tile in zones (default: 8)
200
- firmware_version: Optional firmware version tuple (major, minor).
201
- If not specified, uses 3.70 for extended_multizone
202
- or 2.60 otherwise
203
- storage: Optional storage for persistence
204
- scenario_manager: Optional scenario manager for testing
205
-
206
- Returns:
207
- EmulatedLifxDevice configured for the specified product
208
-
209
- Raises:
210
- ValueError: If product_id is not found in registry
211
-
212
- Examples:
213
- >>> # Create LIFX A19 (PID 27)
214
- >>> device = create_device(27)
215
- >>> # Create LIFX Z strip (PID 32) with 24 zones
216
- >>> strip = create_device(32, zone_count=24)
217
- >>> # Create LIFX Tile (PID 55) with 10 tiles
218
- >>> tiles = create_device(55, tile_count=10)
219
- """
220
- # Get product info from registry
221
- product_info = get_product(product_id)
222
- if product_info is None:
223
- raise ValueError(f"Unknown product ID: {product_id}")
224
-
225
- # Build device using builder pattern
226
- builder = DeviceBuilder(product_info)
227
-
228
- if serial is not None:
229
- builder.with_serial(serial)
230
-
231
- if zone_count is not None:
232
- builder.with_zone_count(zone_count)
233
-
234
- if extended_multizone is not None:
235
- builder.with_extended_multizone(extended_multizone)
236
-
237
- if tile_count is not None:
238
- builder.with_tile_count(tile_count)
239
-
240
- if tile_width is not None and tile_height is not None:
241
- builder.with_tile_dimensions(tile_width, tile_height)
242
-
243
- if firmware_version is not None:
244
- builder.with_firmware_version(*firmware_version)
245
-
246
- if storage is not None:
247
- builder.with_storage(storage)
248
-
249
- if scenario_manager is not None:
250
- builder.with_scenario_manager(scenario_manager)
251
-
252
- return builder.build()
@@ -1,77 +0,0 @@
1
- """Firmware version configuration for devices."""
2
-
3
- from __future__ import annotations
4
-
5
- from lifx_emulator.products.specs import get_default_firmware_version
6
-
7
-
8
- class FirmwareConfig:
9
- """Determines firmware versions for devices.
10
-
11
- Extended multizone support requires firmware 3.70+.
12
- Devices without extended multizone use firmware 2.60.
13
-
14
- Examples:
15
- >>> config = FirmwareConfig()
16
- >>> major, minor = config.get_firmware_version(extended_multizone=True)
17
- >>> (major, minor)
18
- (3, 70)
19
- >>> major, minor = config.get_firmware_version(extended_multizone=False)
20
- >>> (major, minor)
21
- (2, 60)
22
- """
23
-
24
- # Firmware versions
25
- VERSION_EXTENDED = (3, 70) # Extended multizone support
26
- VERSION_LEGACY = (2, 60) # Legacy firmware
27
-
28
- def get_firmware_version(
29
- self,
30
- product_id: int | None = None,
31
- extended_multizone: bool | None = None,
32
- override: tuple[int, int] | None = None,
33
- ) -> tuple[int, int]:
34
- """Get firmware version based on product specs or extended multizone support.
35
-
36
- Precedence order:
37
- 1. Explicit override parameter
38
- 2. Product-specific default from specs.yml
39
- 3. Extended multizone flag (3.70 for True/None, 2.60 for False)
40
-
41
- Args:
42
- product_id: Optional product ID to check specs for defaults
43
- extended_multizone: Whether device supports extended multizone.
44
- None or True defaults to 3.70, False gives 2.60
45
- override: Optional explicit firmware version to use
46
-
47
- Returns:
48
- Tuple of (major, minor) firmware version
49
-
50
- Examples:
51
- >>> config = FirmwareConfig()
52
- >>> config.get_firmware_version(extended_multizone=True)
53
- (3, 70)
54
- >>> config.get_firmware_version(extended_multizone=False)
55
- (2, 60)
56
- >>> config.get_firmware_version(override=(4, 0))
57
- (4, 0)
58
- >>> # With product_id, uses specs if defined
59
- >>> config.get_firmware_version(product_id=27) # doctest: +SKIP
60
- (3, 70)
61
- """
62
- # Explicit override takes precedence
63
- if override is not None:
64
- return override
65
-
66
- # Check product-specific defaults from specs
67
- if product_id is not None:
68
- specs_version = get_default_firmware_version(product_id)
69
- if specs_version is not None:
70
- return specs_version
71
-
72
- # None or True defaults to extended (3.70)
73
- # Only explicit False gives legacy (2.60)
74
- if extended_multizone is False:
75
- return self.VERSION_LEGACY
76
- else:
77
- return self.VERSION_EXTENDED
@@ -1,82 +0,0 @@
1
- """Serial number generation service for LIFX devices."""
2
-
3
- from __future__ import annotations
4
-
5
- import random
6
- from typing import TYPE_CHECKING
7
-
8
- if TYPE_CHECKING:
9
- from lifx_emulator.products.registry import ProductInfo
10
-
11
-
12
- class SerialGenerator:
13
- """Generates serial numbers for emulated LIFX devices.
14
-
15
- Serial numbers are 12-character hex strings with different prefixes
16
- based on device capabilities for easier identification.
17
-
18
- Prefixes:
19
- - d073d9: Matrix/Tile devices
20
- - d073d8: Multizone devices (strips/beams)
21
- - d073d7: HEV devices
22
- - d073d6: Infrared devices
23
- - d073d5: Regular color/temperature lights
24
-
25
- Examples:
26
- >>> generator = SerialGenerator()
27
- >>> serial = generator.generate(product_info)
28
- >>> len(serial)
29
- 12
30
- >>> serial.startswith("d073d")
31
- True
32
- """
33
-
34
- # Device type prefixes for easy identification
35
- PREFIX_MATRIX = "d073d9"
36
- PREFIX_MULTIZONE = "d073d8"
37
- PREFIX_HEV = "d073d7"
38
- PREFIX_INFRARED = "d073d6"
39
- PREFIX_DEFAULT = "d073d5"
40
-
41
- def generate(self, product_info: ProductInfo) -> str:
42
- """Generate a serial number based on product capabilities.
43
-
44
- Args:
45
- product_info: Product information from registry
46
-
47
- Returns:
48
- 12-character hex serial number
49
-
50
- Examples:
51
- >>> from lifx_emulator.products.registry import get_product
52
- >>> generator = SerialGenerator()
53
- >>> product = get_product(55) # LIFX Tile
54
- >>> serial = generator.generate(product)
55
- >>> serial.startswith("d073d9") # Matrix prefix
56
- True
57
- """
58
- prefix = self._determine_prefix(product_info)
59
- suffix = random.randint(100000, 999999) # nosec
60
- return f"{prefix}{suffix:06x}"
61
-
62
- def _determine_prefix(self, product_info: ProductInfo) -> str:
63
- """Determine the prefix based on product capabilities.
64
-
65
- Precedence: matrix > multizone > hev > infrared > default
66
-
67
- Args:
68
- product_info: Product information from registry
69
-
70
- Returns:
71
- 6-character hex prefix
72
- """
73
- if product_info.has_matrix:
74
- return self.PREFIX_MATRIX
75
- elif product_info.has_multizone:
76
- return self.PREFIX_MULTIZONE
77
- elif product_info.has_hev:
78
- return self.PREFIX_HEV
79
- elif product_info.has_infrared:
80
- return self.PREFIX_INFRARED
81
- else:
82
- return self.PREFIX_DEFAULT
@@ -1,39 +0,0 @@
1
- """Packet handler infrastructure using Strategy pattern.
2
-
3
- This module provides the base classes and registry for handling LIFX protocol packets.
4
- Each packet type has a dedicated handler class that implements the business logic.
5
- """
6
-
7
- from lifx_emulator.handlers.base import PacketHandler
8
- from lifx_emulator.handlers.device_handlers import ALL_DEVICE_HANDLERS
9
- from lifx_emulator.handlers.light_handlers import ALL_LIGHT_HANDLERS
10
- from lifx_emulator.handlers.multizone_handlers import ALL_MULTIZONE_HANDLERS
11
- from lifx_emulator.handlers.registry import HandlerRegistry
12
- from lifx_emulator.handlers.tile_handlers import ALL_TILE_HANDLERS
13
-
14
- __all__ = [
15
- "PacketHandler",
16
- "HandlerRegistry",
17
- "ALL_DEVICE_HANDLERS",
18
- "ALL_LIGHT_HANDLERS",
19
- "ALL_MULTIZONE_HANDLERS",
20
- "ALL_TILE_HANDLERS",
21
- "create_default_registry",
22
- ]
23
-
24
-
25
- def create_default_registry() -> HandlerRegistry:
26
- """Create a handler registry with all default handlers registered.
27
-
28
- Returns:
29
- HandlerRegistry with all built-in handlers
30
- """
31
- registry = HandlerRegistry()
32
-
33
- # Register all handler categories
34
- registry.register_all(ALL_DEVICE_HANDLERS)
35
- registry.register_all(ALL_LIGHT_HANDLERS)
36
- registry.register_all(ALL_MULTIZONE_HANDLERS)
37
- registry.register_all(ALL_TILE_HANDLERS)
38
-
39
- return registry
@@ -1,49 +0,0 @@
1
- """Base classes for packet handlers."""
2
-
3
- from __future__ import annotations
4
-
5
- from abc import ABC, abstractmethod
6
- from typing import TYPE_CHECKING, Any
7
-
8
- if TYPE_CHECKING:
9
- from lifx_emulator.devices import DeviceState
10
-
11
-
12
- class PacketHandler(ABC):
13
- """Base class for all packet handlers.
14
-
15
- Each handler implements the logic for processing a specific packet type
16
- and optionally generating a response packet.
17
-
18
- Handlers are stateless and operate on the provided DeviceState.
19
- """
20
-
21
- # Subclasses must define the packet type they handle
22
- PKT_TYPE: int
23
-
24
- @abstractmethod
25
- def handle(
26
- self, device_state: DeviceState, packet: Any | None, res_required: bool
27
- ) -> list[Any]:
28
- """Handle the packet and return response packet(s).
29
-
30
- Args:
31
- device_state: Current device state to read/modify
32
- packet: Unpacked packet object (None for packets with no payload)
33
- res_required: Whether client requested a response (res_required
34
- flag from header)
35
-
36
- Returns:
37
- List of response packets (empty list if no response needed).
38
- This unified return type simplifies packet processing logic.
39
-
40
- Notes:
41
- - Handlers should modify device_state directly for SET operations
42
- - Handlers should check device capabilities before processing
43
- - Return empty list [] if the device doesn't support this packet type
44
- - Always return a list, even for single responses: [packet]
45
- """
46
- pass
47
-
48
- def __repr__(self) -> str:
49
- return f"{self.__class__.__name__}(PKT_TYPE={self.PKT_TYPE})"