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,375 +0,0 @@
1
- """Device builder with fluent API for creating emulated LIFX devices."""
2
-
3
- from __future__ import annotations
4
-
5
- import time
6
- from typing import TYPE_CHECKING
7
-
8
- from lifx_emulator.devices import DeviceState, EmulatedLifxDevice
9
- from lifx_emulator.devices.state_restorer import StateRestorer
10
- from lifx_emulator.devices.states import (
11
- CoreDeviceState,
12
- GroupState,
13
- HevState,
14
- InfraredState,
15
- LocationState,
16
- MatrixState,
17
- MultiZoneState,
18
- NetworkState,
19
- WaveformState,
20
- )
21
- from lifx_emulator.factories.default_config import DefaultColorConfig
22
- from lifx_emulator.factories.firmware_config import FirmwareConfig
23
- from lifx_emulator.factories.serial_generator import SerialGenerator
24
- from lifx_emulator.products.specs import (
25
- get_default_tile_count,
26
- get_default_zone_count,
27
- get_tile_dimensions,
28
- )
29
- from lifx_emulator.protocol.protocol_types import LightHsbk
30
-
31
- if TYPE_CHECKING:
32
- from lifx_emulator.devices import DevicePersistenceAsyncFile
33
- from lifx_emulator.products.registry import ProductInfo
34
- from lifx_emulator.scenarios import HierarchicalScenarioManager
35
-
36
-
37
- class DeviceBuilder:
38
- """Fluent API builder for creating emulated LIFX devices.
39
-
40
- This builder separates device construction into discrete, testable steps:
41
- 1. Product configuration (serial, firmware, color)
42
- 2. Capability-specific configuration (zones, tiles)
43
- 3. State composition
44
- 4. Device creation
45
-
46
- Examples:
47
- >>> from lifx_emulator.products.registry import get_product
48
- >>> product = get_product(27) # LIFX A19
49
- >>> builder = DeviceBuilder(product)
50
- >>> device = builder.with_serial("d073d5000001").build()
51
-
52
- >>> # Multizone device with custom zones
53
- >>> product = get_product(32) # LIFX Z
54
- >>> device = (
55
- ... DeviceBuilder(product)
56
- ... .with_zone_count(24)
57
- ... .with_extended_multizone(False)
58
- ... .build()
59
- ... )
60
- """
61
-
62
- def __init__(self, product_info: ProductInfo):
63
- """Initialize builder with product information.
64
-
65
- Args:
66
- product_info: Product information from registry
67
- """
68
- self._product_info = product_info
69
-
70
- # Configuration state
71
- self._serial: str | None = None
72
- self._zone_count: int | None = None
73
- self._extended_multizone: bool | None = None
74
- self._tile_count: int | None = None
75
- self._tile_width: int | None = None
76
- self._tile_height: int | None = None
77
- self._firmware_version: tuple[int, int] | None = None
78
- self._storage: DevicePersistenceAsyncFile | None = None
79
- self._scenario_manager: HierarchicalScenarioManager | None = None
80
- self._color: LightHsbk | None = None
81
-
82
- # Helper services
83
- self._serial_generator = SerialGenerator()
84
- self._color_config = DefaultColorConfig()
85
- self._firmware_config = FirmwareConfig()
86
-
87
- def with_serial(self, serial: str) -> DeviceBuilder:
88
- """Set device serial number.
89
-
90
- Args:
91
- serial: 12-character hex serial number
92
-
93
- Returns:
94
- Self for method chaining
95
- """
96
- self._serial = serial
97
- return self
98
-
99
- def with_zone_count(self, zone_count: int) -> DeviceBuilder:
100
- """Set zone count for multizone devices.
101
-
102
- Args:
103
- zone_count: Number of zones
104
-
105
- Returns:
106
- Self for method chaining
107
- """
108
- self._zone_count = zone_count
109
- return self
110
-
111
- def with_extended_multizone(self, extended: bool) -> DeviceBuilder:
112
- """Enable/disable extended multizone support.
113
-
114
- Args:
115
- extended: Whether to enable extended multizone
116
-
117
- Returns:
118
- Self for method chaining
119
- """
120
- self._extended_multizone = extended
121
- return self
122
-
123
- def with_tile_count(self, tile_count: int) -> DeviceBuilder:
124
- """Set tile count for matrix devices.
125
-
126
- Args:
127
- tile_count: Number of tiles
128
-
129
- Returns:
130
- Self for method chaining
131
- """
132
- self._tile_count = tile_count
133
- return self
134
-
135
- def with_tile_dimensions(self, width: int, height: int) -> DeviceBuilder:
136
- """Set tile dimensions for matrix devices.
137
-
138
- Args:
139
- width: Tile width in zones
140
- height: Tile height in zones
141
-
142
- Returns:
143
- Self for method chaining
144
- """
145
- self._tile_width = width
146
- self._tile_height = height
147
- return self
148
-
149
- def with_firmware_version(self, major: int, minor: int) -> DeviceBuilder:
150
- """Set firmware version.
151
-
152
- Args:
153
- major: Major version number
154
- minor: Minor version number
155
-
156
- Returns:
157
- Self for method chaining
158
- """
159
- self._firmware_version = (major, minor)
160
- return self
161
-
162
- def with_storage(self, storage: DevicePersistenceAsyncFile) -> DeviceBuilder:
163
- """Enable persistent storage.
164
-
165
- Args:
166
- storage: Async storage backend
167
-
168
- Returns:
169
- Self for method chaining
170
- """
171
- self._storage = storage
172
- return self
173
-
174
- def with_scenario_manager(
175
- self, scenario_manager: HierarchicalScenarioManager
176
- ) -> DeviceBuilder:
177
- """Set scenario manager for testing.
178
-
179
- Args:
180
- scenario_manager: Scenario manager instance
181
-
182
- Returns:
183
- Self for method chaining
184
- """
185
- self._scenario_manager = scenario_manager
186
- return self
187
-
188
- def with_color(self, color: LightHsbk) -> DeviceBuilder:
189
- """Set initial device color.
190
-
191
- Args:
192
- color: Initial color
193
-
194
- Returns:
195
- Self for method chaining
196
- """
197
- self._color = color
198
- return self
199
-
200
- def build(self) -> EmulatedLifxDevice:
201
- """Build the emulated device.
202
-
203
- Returns:
204
- Configured EmulatedLifxDevice instance
205
- """
206
- # 1. Generate/validate serial
207
- serial = self._serial or self._serial_generator.generate(self._product_info)
208
-
209
- # 2. Apply product-specific defaults
210
- self._apply_product_defaults()
211
-
212
- # 3. Determine firmware version
213
- version_major, version_minor = self._firmware_config.get_firmware_version(
214
- product_id=self._product_info.pid,
215
- extended_multizone=self._extended_multizone,
216
- override=self._firmware_version,
217
- )
218
-
219
- # 4. Get default color
220
- color = self._color or self._color_config.get_default_color(self._product_info)
221
-
222
- # 5. Create core state
223
- core = self._create_core_state(serial, color, version_major, version_minor)
224
-
225
- # 6. Create basic states
226
- network = NetworkState()
227
- location = LocationState()
228
- group = GroupState()
229
- waveform = WaveformState()
230
-
231
- # 7. Create capability-specific states
232
- infrared_state = self._create_infrared_state()
233
- hev_state = self._create_hev_state()
234
- multizone_state = self._create_multizone_state()
235
- matrix_state = self._create_matrix_state()
236
-
237
- # 8. Determine extended multizone support
238
- firmware_version_int = (version_major << 16) | version_minor
239
- has_extended_multizone = self._product_info.supports_extended_multizone(
240
- firmware_version_int
241
- )
242
-
243
- # 9. Compose device state
244
- state = DeviceState(
245
- core=core,
246
- network=network,
247
- location=location,
248
- group=group,
249
- waveform=waveform,
250
- infrared=infrared_state,
251
- hev=hev_state,
252
- multizone=multizone_state,
253
- matrix=matrix_state,
254
- has_color=self._product_info.has_color,
255
- has_infrared=self._product_info.has_infrared,
256
- has_multizone=self._product_info.has_multizone,
257
- has_extended_multizone=has_extended_multizone,
258
- has_matrix=self._product_info.has_matrix,
259
- has_hev=self._product_info.has_hev,
260
- has_relays=self._product_info.has_relays,
261
- has_buttons=self._product_info.has_buttons,
262
- )
263
-
264
- # 10. Restore saved state if persistence enabled
265
- if self._storage:
266
- restorer = StateRestorer(self._storage)
267
- restorer.restore_if_available(state)
268
-
269
- # 11. Create device
270
- return EmulatedLifxDevice(
271
- state, storage=self._storage, scenario_manager=self._scenario_manager
272
- )
273
-
274
- def _apply_product_defaults(self):
275
- """Apply product-specific defaults from specs."""
276
- # Zone count for multizone devices
277
- if self._product_info.has_multizone and self._zone_count is None:
278
- self._zone_count = get_default_zone_count(self._product_info.pid) or 16
279
-
280
- # Tile configuration for matrix devices
281
- if self._product_info.has_matrix:
282
- # Get tile dimensions from specs (always use specs for dimensions)
283
- tile_dims = get_tile_dimensions(self._product_info.pid)
284
- if tile_dims:
285
- self._tile_width, self._tile_height = tile_dims
286
- else:
287
- # Fallback to standard 8x8 tiles
288
- if self._tile_width is None:
289
- self._tile_width = 8
290
- if self._tile_height is None:
291
- self._tile_height = 8
292
-
293
- # Get default tile count from specs
294
- if self._tile_count is None:
295
- specs_tile_count = get_default_tile_count(self._product_info.pid)
296
- self._tile_count = (
297
- specs_tile_count if specs_tile_count is not None else 5
298
- )
299
-
300
- def _create_core_state(
301
- self, serial: str, color: LightHsbk, version_major: int, version_minor: int
302
- ) -> CoreDeviceState:
303
- """Create core device state.
304
-
305
- Args:
306
- serial: Device serial number
307
- color: Initial color
308
- version_major: Firmware major version
309
- version_minor: Firmware minor version
310
-
311
- Returns:
312
- CoreDeviceState instance
313
- """
314
- label = f"{self._product_info.name} {serial[-6:]}"
315
-
316
- return CoreDeviceState(
317
- serial=serial,
318
- label=label,
319
- power_level=65535, # Default to on
320
- color=color,
321
- vendor=self._product_info.vendor,
322
- product=self._product_info.pid,
323
- version_major=version_major,
324
- version_minor=version_minor,
325
- build_timestamp=int(time.time()),
326
- mac_address=bytes.fromhex(serial[:12]),
327
- )
328
-
329
- def _create_infrared_state(self) -> InfraredState | None:
330
- """Create infrared state if product has infrared capability.
331
-
332
- Returns:
333
- InfraredState instance or None
334
- """
335
- if self._product_info.has_infrared:
336
- return InfraredState(infrared_brightness=16384)
337
- return None
338
-
339
- def _create_hev_state(self) -> HevState | None:
340
- """Create HEV state if product has HEV capability.
341
-
342
- Returns:
343
- HevState instance or None
344
- """
345
- if self._product_info.has_hev:
346
- return HevState()
347
- return None
348
-
349
- def _create_multizone_state(self) -> MultiZoneState | None:
350
- """Create multizone state if product has multizone capability.
351
-
352
- Returns:
353
- MultiZoneState instance or None
354
- """
355
- if self._product_info.has_multizone and self._zone_count:
356
- return MultiZoneState(
357
- zone_count=self._zone_count,
358
- zone_colors=[], # Will be initialized by EmulatedLifxDevice
359
- )
360
- return None
361
-
362
- def _create_matrix_state(self) -> MatrixState | None:
363
- """Create matrix state if product has matrix capability.
364
-
365
- Returns:
366
- MatrixState instance or None
367
- """
368
- if self._product_info.has_matrix and self._tile_count:
369
- return MatrixState(
370
- tile_count=self._tile_count,
371
- tile_devices=[], # Will be initialized by EmulatedLifxDevice
372
- tile_width=self._tile_width or 8,
373
- tile_height=self._tile_height or 8,
374
- )
375
- return None
@@ -1,158 +0,0 @@
1
- """Default configuration helpers for device factory."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import TYPE_CHECKING
6
-
7
- from lifx_emulator.protocol.protocol_types import LightHsbk
8
-
9
- if TYPE_CHECKING:
10
- from lifx_emulator.products.registry import ProductInfo
11
-
12
-
13
- class DefaultColorConfig:
14
- """Determines default colors for devices based on product capabilities.
15
-
16
- Different device types get unique default colors for visual identification:
17
- - Brightness-only: White at 2700K
18
- - Color temperature: White at 3500K (middle of range)
19
- - Matrix devices: Cyan
20
- - Multizone devices: Red
21
- - HEV devices: Green
22
- - Infrared devices: Red
23
- - Color devices: Orange
24
-
25
- Examples:
26
- >>> from lifx_emulator.products.registry import get_product
27
- >>> config = DefaultColorConfig()
28
- >>> product = get_product(27) # LIFX A19 (color)
29
- >>> color = config.get_default_color(product)
30
- >>> color.saturation
31
- 65535
32
- """
33
-
34
- # Hue values for different device types (0-65535)
35
- HUE_CYAN = 43690
36
- HUE_RED = 0
37
- HUE_GREEN = 32768
38
- HUE_ORANGE = 21845
39
-
40
- # Default brightness (50%)
41
- DEFAULT_BRIGHTNESS = 32768
42
-
43
- # Default kelvin values
44
- KELVIN_WARM = 2700
45
- KELVIN_NEUTRAL = 3500
46
-
47
- def get_default_color(self, product_info: ProductInfo) -> LightHsbk:
48
- """Get default color for a product.
49
-
50
- Args:
51
- product_info: Product information from registry
52
-
53
- Returns:
54
- Default LightHsbk color for the device
55
- """
56
- if not product_info.has_color and self._is_brightness_only(product_info):
57
- return self._brightness_only_color()
58
- elif not product_info.has_color and self._is_temperature_adjustable(
59
- product_info
60
- ):
61
- return self._temperature_adjustable_color()
62
- else:
63
- return self._color_device_color(product_info)
64
-
65
- def _is_brightness_only(self, product_info: ProductInfo) -> bool:
66
- """Check if device has fixed color temperature.
67
-
68
- Args:
69
- product_info: Product information from registry
70
-
71
- Returns:
72
- True if temperature range has identical min/max
73
- """
74
- return (
75
- product_info.temperature_range is not None
76
- and product_info.temperature_range.min == product_info.temperature_range.max
77
- )
78
-
79
- def _is_temperature_adjustable(self, product_info: ProductInfo) -> bool:
80
- """Check if device has adjustable color temperature.
81
-
82
- Args:
83
- product_info: Product information from registry
84
-
85
- Returns:
86
- True if temperature range has different min/max
87
- """
88
- return (
89
- product_info.temperature_range is not None
90
- and product_info.temperature_range.min != product_info.temperature_range.max
91
- )
92
-
93
- def _brightness_only_color(self) -> LightHsbk:
94
- """Get default color for brightness-only devices.
95
-
96
- Returns:
97
- White at 2700K with 50% brightness
98
- """
99
- return LightHsbk(
100
- hue=0,
101
- saturation=0,
102
- brightness=self.DEFAULT_BRIGHTNESS,
103
- kelvin=self.KELVIN_WARM,
104
- )
105
-
106
- def _temperature_adjustable_color(self) -> LightHsbk:
107
- """Get default color for temperature-adjustable devices.
108
-
109
- Returns:
110
- White at 3500K with 50% brightness
111
- """
112
- return LightHsbk(
113
- hue=0,
114
- saturation=0,
115
- brightness=self.DEFAULT_BRIGHTNESS,
116
- kelvin=self.KELVIN_NEUTRAL,
117
- )
118
-
119
- def _color_device_color(self, product_info: ProductInfo) -> LightHsbk:
120
- """Get default color for color-capable devices.
121
-
122
- Different hues based on device capabilities for easy visual identification.
123
-
124
- Args:
125
- product_info: Product information from registry
126
-
127
- Returns:
128
- Colored LightHsbk with full saturation
129
- """
130
- hue = self._get_hue_for_capability(product_info)
131
- return LightHsbk(
132
- hue=hue,
133
- saturation=65535,
134
- brightness=self.DEFAULT_BRIGHTNESS,
135
- kelvin=self.KELVIN_NEUTRAL,
136
- )
137
-
138
- def _get_hue_for_capability(self, product_info: ProductInfo) -> int:
139
- """Determine hue based on product capabilities.
140
-
141
- Precedence: matrix > multizone > hev > infrared > default
142
-
143
- Args:
144
- product_info: Product information from registry
145
-
146
- Returns:
147
- Hue value (0-65535)
148
- """
149
- if product_info.has_matrix:
150
- return self.HUE_CYAN
151
- elif product_info.has_multizone:
152
- return self.HUE_RED
153
- elif product_info.has_hev:
154
- return self.HUE_GREEN
155
- elif product_info.has_infrared:
156
- return self.HUE_RED
157
- else:
158
- return self.HUE_ORANGE