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,503 +0,0 @@
1
- """Light packet handlers."""
2
-
3
- from __future__ import annotations
4
-
5
- import logging
6
- from typing import TYPE_CHECKING, Any
7
-
8
- from lifx_emulator.handlers.base import PacketHandler
9
- from lifx_emulator.protocol.packets import Light
10
- from lifx_emulator.protocol.protocol_types import LightHsbk, LightLastHevCycleResult
11
-
12
- if TYPE_CHECKING:
13
- from lifx_emulator.devices import DeviceState
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- def _compute_average_color(colors: list[LightHsbk]) -> LightHsbk:
19
- """Compute average HSBK color from a list of LightHsbk colors.
20
-
21
- Uses circular mean for hue to correctly handle hue wraparound
22
- (e.g., average of 10° and 350° is 0°, not 180°).
23
-
24
- Args:
25
- colors: List of LightHsbk colors to average
26
-
27
- Returns:
28
- LightHsbk with averaged values using circular mean for hue
29
- """
30
- import math
31
-
32
- if not colors:
33
- return LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500)
34
-
35
- # Convert uint16 values to proper ranges and calculate circular mean
36
- hue_x_total = 0.0
37
- hue_y_total = 0.0
38
- saturation_total = 0.0
39
- brightness_total = 0.0
40
- kelvin_total = 0
41
-
42
- for color in colors:
43
- # Convert uint16 hue (0-65535) to degrees (0-360)
44
- hue_deg = round(float(color.hue) * 360 / 0x10000, 2)
45
-
46
- # Convert uint16 sat/bright (0-65535) to float (0-1)
47
- sat_float = round(float(color.saturation) / 0xFFFF, 4)
48
- bright_float = round(float(color.brightness) / 0xFFFF, 4)
49
-
50
- # Circular mean calculation for hue using sin/cos
51
- hue_x_total += math.sin(hue_deg * 2.0 * math.pi / 360)
52
- hue_y_total += math.cos(hue_deg * 2.0 * math.pi / 360)
53
-
54
- # Regular sums for other components
55
- saturation_total += sat_float
56
- brightness_total += bright_float
57
- kelvin_total += color.kelvin
58
-
59
- # Calculate circular mean for hue
60
- hue = math.atan2(hue_x_total, hue_y_total) / (2.0 * math.pi)
61
- if hue < 0.0:
62
- hue += 1.0
63
- hue *= 360
64
- hue = round(hue, 4)
65
-
66
- # Calculate arithmetic means for other components
67
- saturation = round(saturation_total / len(colors), 4)
68
- brightness = round(brightness_total / len(colors), 4)
69
- kelvin = round(kelvin_total / len(colors))
70
-
71
- # Convert back to uint16 values
72
- uint16_hue = int(round(0x10000 * hue) / 360) % 0x10000
73
- uint16_saturation = int(round(0xFFFF * saturation))
74
- uint16_brightness = int(round(0xFFFF * brightness))
75
-
76
- return LightHsbk(
77
- hue=uint16_hue,
78
- saturation=uint16_saturation,
79
- brightness=uint16_brightness,
80
- kelvin=kelvin,
81
- )
82
-
83
-
84
- class GetColorHandler(PacketHandler):
85
- """Handle LightGet (101) -> LightState (107)."""
86
-
87
- PKT_TYPE = Light.GetColor.PKT_TYPE
88
-
89
- def handle(
90
- self, device_state: DeviceState, packet: Any | None, res_required: bool
91
- ) -> list[Any]:
92
- # For multizone/matrix devices, compute average color from all zones
93
- # This provides backwards compatibility with clients that don't use
94
- # zone-specific or tile-specific packets
95
- color_to_return = device_state.color
96
-
97
- if device_state.has_multizone and device_state.zone_colors:
98
- # Return average of all zone colors
99
- color_to_return = _compute_average_color(device_state.zone_colors)
100
- elif device_state.has_matrix and device_state.tile_devices:
101
- # Collect all zone colors from all tiles
102
- all_zones = []
103
- for tile in device_state.tile_devices:
104
- all_zones.extend(tile["colors"])
105
- color_to_return = _compute_average_color(all_zones)
106
-
107
- return [
108
- Light.StateColor(
109
- color=color_to_return,
110
- power=device_state.power_level,
111
- label=device_state.label,
112
- )
113
- ]
114
-
115
-
116
- class SetColorHandler(PacketHandler):
117
- """Handle LightSetColor (102) -> LightState (107)."""
118
-
119
- PKT_TYPE = Light.SetColor.PKT_TYPE
120
-
121
- def handle(
122
- self,
123
- device_state: DeviceState,
124
- packet: Light.SetColor | None,
125
- res_required: bool,
126
- ) -> list[Any]:
127
- if packet:
128
- device_state.color = packet.color
129
- c = packet.color
130
-
131
- # For backwards compatibility: propagate color to all zones
132
- # Multizone devices: update all zone colors
133
- if device_state.has_multizone and device_state.zone_colors:
134
- for i in range(len(device_state.zone_colors)):
135
- device_state.zone_colors[i] = packet.color
136
- logger.info(
137
- f"Color set to HSBK({c.hue}, {c.saturation}, "
138
- f"{c.brightness}, {c.kelvin}) across all "
139
- f"{len(device_state.zone_colors)} zones, "
140
- f"duration={packet.duration}ms"
141
- )
142
- # Matrix devices: update all tile zones
143
- elif device_state.has_matrix and device_state.tile_devices:
144
- total_zones = 0
145
- for tile in device_state.tile_devices:
146
- for i in range(len(tile["colors"])):
147
- tile["colors"][i] = packet.color
148
- total_zones += len(tile["colors"])
149
- logger.info(
150
- f"Color set to HSBK({c.hue}, {c.saturation}, "
151
- f"{c.brightness}, {c.kelvin}) across all {total_zones} zones, "
152
- f"duration={packet.duration}ms"
153
- )
154
- else:
155
- # Simple color device
156
- logger.info(
157
- f"Color set to HSBK({c.hue}, {c.saturation}, "
158
- f"{c.brightness}, {c.kelvin}), duration={packet.duration}ms"
159
- )
160
-
161
- if res_required:
162
- return [
163
- Light.StateColor(
164
- color=device_state.color,
165
- power=device_state.power_level,
166
- label=device_state.label,
167
- )
168
- ]
169
- return []
170
-
171
-
172
- class GetPowerHandler(PacketHandler):
173
- """Handle LightGetPower (116) -> LightStatePower (118)."""
174
-
175
- PKT_TYPE = Light.GetPower.PKT_TYPE
176
-
177
- def handle(
178
- self, device_state: DeviceState, packet: Any | None, res_required: bool
179
- ) -> list[Any]:
180
- return [Light.StatePower(level=device_state.power_level)]
181
-
182
-
183
- class SetPowerHandler(PacketHandler):
184
- """Handle LightSetPower (117) -> LightStatePower (118)."""
185
-
186
- PKT_TYPE = Light.SetPower.PKT_TYPE
187
-
188
- def handle(
189
- self,
190
- device_state: DeviceState,
191
- packet: Light.SetPower | None,
192
- res_required: bool,
193
- ) -> list[Any]:
194
- if packet:
195
- device_state.power_level = packet.level
196
- logger.info(
197
- f"Light power set to {packet.level}, duration={packet.duration}ms"
198
- )
199
-
200
- if res_required:
201
- return [Light.StatePower(level=device_state.power_level)]
202
- return []
203
-
204
-
205
- class SetWaveformHandler(PacketHandler):
206
- """Handle LightSetWaveform (103) -> LightState (107)."""
207
-
208
- PKT_TYPE = Light.SetWaveform.PKT_TYPE
209
-
210
- def handle(
211
- self,
212
- device_state: DeviceState,
213
- packet: Light.SetWaveform | None,
214
- res_required: bool,
215
- ) -> list[Any]:
216
- if packet:
217
- # Store waveform state
218
- device_state.waveform_active = True
219
- device_state.waveform_transient = packet.transient
220
- device_state.waveform_color = packet.color
221
- device_state.waveform_period_ms = packet.period
222
- device_state.waveform_cycles = packet.cycles
223
- device_state.waveform_skew_ratio = packet.skew_ratio
224
- device_state.waveform_type = int(packet.waveform)
225
-
226
- # If not transient, update the color state
227
- if not packet.transient:
228
- device_state.color = packet.color
229
-
230
- # For backwards compatibility: propagate color to all zones
231
- # Multizone devices: update all zone colors
232
- if device_state.has_multizone and device_state.zone_colors:
233
- for i in range(len(device_state.zone_colors)):
234
- device_state.zone_colors[i] = packet.color
235
- # Matrix devices: update all tile zones
236
- elif device_state.has_matrix and device_state.tile_devices:
237
- for tile in device_state.tile_devices:
238
- for i in range(len(tile["colors"])):
239
- tile["colors"][i] = packet.color
240
-
241
- logger.info(
242
- f"Waveform set: type={packet.waveform}, "
243
- f"transient={packet.transient}, period={packet.period}ms, "
244
- f"cycles={packet.cycles}, skew={packet.skew_ratio}"
245
- )
246
-
247
- if res_required:
248
- # Use GetColorHandler to get proper averaged color if needed
249
- handler = GetColorHandler()
250
- return handler.handle(device_state, None, res_required)
251
- return []
252
-
253
-
254
- class SetWaveformOptionalHandler(PacketHandler):
255
- """Handle LightSetWaveformOptional (119) -> LightState (107)."""
256
-
257
- PKT_TYPE = Light.SetWaveformOptional.PKT_TYPE
258
-
259
- def handle(
260
- self,
261
- device_state: DeviceState,
262
- packet: Light.SetWaveformOptional | None,
263
- res_required: bool,
264
- ) -> list[Any]:
265
- if packet:
266
- # Store waveform state
267
- device_state.waveform_active = True
268
- device_state.waveform_transient = packet.transient
269
- device_state.waveform_period_ms = packet.period
270
- device_state.waveform_cycles = packet.cycles
271
- device_state.waveform_skew_ratio = packet.skew_ratio
272
- device_state.waveform_type = int(packet.waveform)
273
-
274
- # Apply color components selectively based on flags
275
- if not packet.transient:
276
- if packet.set_hue:
277
- device_state.color.hue = packet.color.hue
278
- if packet.set_saturation:
279
- device_state.color.saturation = packet.color.saturation
280
- if packet.set_brightness:
281
- device_state.color.brightness = packet.color.brightness
282
- if packet.set_kelvin:
283
- device_state.color.kelvin = packet.color.kelvin
284
-
285
- # Backwards compatibility propagates color changes to zones
286
- # Multizone devices: update all zone colors
287
- if device_state.has_multizone and device_state.zone_colors:
288
- for zone_color in device_state.zone_colors:
289
- if packet.set_hue:
290
- zone_color.hue = packet.color.hue
291
- if packet.set_saturation:
292
- zone_color.saturation = packet.color.saturation
293
- if packet.set_brightness:
294
- zone_color.brightness = packet.color.brightness
295
- if packet.set_kelvin:
296
- zone_color.kelvin = packet.color.kelvin
297
- # Matrix devices: update all tile zones
298
- elif device_state.has_matrix and device_state.tile_devices:
299
- for tile in device_state.tile_devices:
300
- for zone_color in tile["colors"]:
301
- if packet.set_hue:
302
- zone_color.hue = packet.color.hue
303
- if packet.set_saturation:
304
- zone_color.saturation = packet.color.saturation
305
- if packet.set_brightness:
306
- zone_color.brightness = packet.color.brightness
307
- if packet.set_kelvin:
308
- zone_color.kelvin = packet.color.kelvin
309
-
310
- # Store the waveform color (all components)
311
- device_state.waveform_color = packet.color
312
-
313
- logger.info(
314
- f"Waveform optional set: type={packet.waveform}, "
315
- f"transient={packet.transient}, period={packet.period}ms, "
316
- f"cycles={packet.cycles}, components=[H:{packet.set_hue},"
317
- f"S:{packet.set_saturation},B:{packet.set_brightness},"
318
- f"K:{packet.set_kelvin}]"
319
- )
320
-
321
- if res_required:
322
- # Use GetColorHandler to get proper averaged color if needed
323
- handler = GetColorHandler()
324
- return handler.handle(device_state, None, res_required)
325
- return []
326
-
327
-
328
- class GetInfraredHandler(PacketHandler):
329
- """Handle LightGetInfrared (120) -> LightStateInfrared (121)."""
330
-
331
- PKT_TYPE = Light.GetInfrared.PKT_TYPE
332
-
333
- def handle(
334
- self, device_state: DeviceState, packet: Any | None, res_required: bool
335
- ) -> list[Any]:
336
- if not device_state.has_infrared:
337
- return []
338
- return [Light.StateInfrared(brightness=device_state.infrared_brightness)]
339
-
340
-
341
- class SetInfraredHandler(PacketHandler):
342
- """Handle LightSetInfrared (122) -> LightStateInfrared (121)."""
343
-
344
- PKT_TYPE = Light.SetInfrared.PKT_TYPE
345
-
346
- def handle(
347
- self,
348
- device_state: DeviceState,
349
- packet: Light.SetInfrared | None,
350
- res_required: bool,
351
- ) -> list[Any]:
352
- if not device_state.has_infrared:
353
- return []
354
- if packet:
355
- device_state.infrared_brightness = packet.brightness
356
- logger.info("Infrared brightness set to %s", packet.brightness)
357
-
358
- if res_required:
359
- return [Light.StateInfrared(brightness=device_state.infrared_brightness)]
360
- return []
361
-
362
-
363
- class GetHevCycleHandler(PacketHandler):
364
- """Handle LightGetHevCycle (142) -> LightStateHevCycle (144)."""
365
-
366
- PKT_TYPE = Light.GetHevCycle.PKT_TYPE
367
-
368
- def handle(
369
- self, device_state: DeviceState, packet: Any | None, res_required: bool
370
- ) -> list[Any]:
371
- if not device_state.has_hev:
372
- return []
373
- return [
374
- Light.StateHevCycle(
375
- duration_s=device_state.hev_cycle_duration_s,
376
- remaining_s=device_state.hev_cycle_remaining_s,
377
- last_power=device_state.hev_cycle_last_power,
378
- )
379
- ]
380
-
381
-
382
- class SetHevCycleHandler(PacketHandler):
383
- """Handle LightSetHevCycle (143) -> LightStateHevCycle (144)."""
384
-
385
- PKT_TYPE = Light.SetHevCycle.PKT_TYPE
386
-
387
- def handle(
388
- self,
389
- device_state: DeviceState,
390
- packet: Light.SetHevCycle | None,
391
- res_required: bool,
392
- ) -> list[Any]:
393
- if not device_state.has_hev:
394
- return []
395
- if packet:
396
- device_state.hev_cycle_duration_s = packet.duration_s
397
- if packet.enable:
398
- device_state.hev_cycle_remaining_s = packet.duration_s
399
- else:
400
- device_state.hev_cycle_remaining_s = 0
401
- logger.info(
402
- f"HEV cycle set: enable={packet.enable}, duration={packet.duration_s}s"
403
- )
404
-
405
- if res_required:
406
- return [
407
- Light.StateHevCycle(
408
- duration_s=device_state.hev_cycle_duration_s,
409
- remaining_s=device_state.hev_cycle_remaining_s,
410
- last_power=device_state.hev_cycle_last_power,
411
- )
412
- ]
413
- return []
414
-
415
-
416
- class GetHevCycleConfigurationHandler(PacketHandler):
417
- """Handle LightGetHevCycleConfiguration (145).
418
-
419
- Returns LightStateHevCycleConfiguration (147).
420
- """
421
-
422
- PKT_TYPE = Light.GetHevCycleConfiguration.PKT_TYPE
423
-
424
- def handle(
425
- self, device_state: DeviceState, packet: Any | None, res_required: bool
426
- ) -> list[Any]:
427
- if not device_state.has_hev:
428
- return []
429
- return [
430
- Light.StateHevCycleConfiguration(
431
- indication=device_state.hev_indication,
432
- duration_s=device_state.hev_cycle_duration_s,
433
- )
434
- ]
435
-
436
-
437
- class SetHevCycleConfigurationHandler(PacketHandler):
438
- """Handle LightSetHevCycleConfiguration (146).
439
-
440
- Returns LightStateHevCycleConfiguration (147).
441
- """
442
-
443
- PKT_TYPE = Light.SetHevCycleConfiguration.PKT_TYPE
444
-
445
- def handle(
446
- self,
447
- device_state: DeviceState,
448
- packet: Light.SetHevCycleConfiguration | None,
449
- res_required: bool,
450
- ) -> list[Any]:
451
- if not device_state.has_hev:
452
- return []
453
- if packet:
454
- device_state.hev_indication = packet.indication
455
- device_state.hev_cycle_duration_s = packet.duration_s
456
- logger.info(
457
- f"HEV config set: indication={packet.indication}, "
458
- f"duration={packet.duration_s}s"
459
- )
460
-
461
- if res_required:
462
- return [
463
- Light.StateHevCycleConfiguration(
464
- indication=device_state.hev_indication,
465
- duration_s=device_state.hev_cycle_duration_s,
466
- )
467
- ]
468
- return []
469
-
470
-
471
- class GetLastHevCycleResultHandler(PacketHandler):
472
- """Handle LightGetLastHevCycleResult (148) -> LightStateLastHevCycleResult (149)."""
473
-
474
- PKT_TYPE = Light.GetLastHevCycleResult.PKT_TYPE
475
-
476
- def handle(
477
- self, device_state: DeviceState, packet: Any | None, res_required: bool
478
- ) -> list[Any]:
479
- if not device_state.has_hev:
480
- return []
481
- return [
482
- Light.StateLastHevCycleResult(
483
- result=LightLastHevCycleResult(device_state.hev_last_result)
484
- )
485
- ]
486
-
487
-
488
- # List of all light handlers for easy registration
489
- ALL_LIGHT_HANDLERS = [
490
- GetColorHandler(),
491
- SetColorHandler(),
492
- GetPowerHandler(),
493
- SetPowerHandler(),
494
- SetWaveformHandler(),
495
- SetWaveformOptionalHandler(),
496
- GetInfraredHandler(),
497
- SetInfraredHandler(),
498
- GetHevCycleHandler(),
499
- SetHevCycleHandler(),
500
- GetHevCycleConfigurationHandler(),
501
- SetHevCycleConfigurationHandler(),
502
- GetLastHevCycleResultHandler(),
503
- ]