lifx-emulator 2.4.0__py3-none-any.whl → 3.0.1__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 (68) hide show
  1. lifx_emulator-3.0.1.dist-info/METADATA +102 -0
  2. lifx_emulator-3.0.1.dist-info/RECORD +18 -0
  3. lifx_emulator-3.0.1.dist-info/entry_points.txt +2 -0
  4. lifx_emulator_app/__init__.py +10 -0
  5. {lifx_emulator → lifx_emulator_app}/__main__.py +2 -3
  6. {lifx_emulator → lifx_emulator_app}/api/__init__.py +1 -1
  7. {lifx_emulator → lifx_emulator_app}/api/app.py +3 -3
  8. {lifx_emulator → lifx_emulator_app}/api/mappers/__init__.py +1 -1
  9. {lifx_emulator → lifx_emulator_app}/api/mappers/device_mapper.py +1 -1
  10. {lifx_emulator → lifx_emulator_app}/api/models.py +1 -2
  11. lifx_emulator_app/api/routers/__init__.py +11 -0
  12. {lifx_emulator → lifx_emulator_app}/api/routers/devices.py +2 -2
  13. {lifx_emulator → lifx_emulator_app}/api/routers/monitoring.py +1 -1
  14. {lifx_emulator → lifx_emulator_app}/api/routers/scenarios.py +1 -1
  15. lifx_emulator_app/api/services/__init__.py +8 -0
  16. {lifx_emulator → lifx_emulator_app}/api/services/device_service.py +3 -2
  17. lifx_emulator/__init__.py +0 -31
  18. lifx_emulator/api/routers/__init__.py +0 -11
  19. lifx_emulator/api/services/__init__.py +0 -8
  20. lifx_emulator/constants.py +0 -33
  21. lifx_emulator/devices/__init__.py +0 -37
  22. lifx_emulator/devices/device.py +0 -395
  23. lifx_emulator/devices/manager.py +0 -256
  24. lifx_emulator/devices/observers.py +0 -139
  25. lifx_emulator/devices/persistence.py +0 -308
  26. lifx_emulator/devices/state_restorer.py +0 -259
  27. lifx_emulator/devices/state_serializer.py +0 -157
  28. lifx_emulator/devices/states.py +0 -381
  29. lifx_emulator/factories/__init__.py +0 -39
  30. lifx_emulator/factories/builder.py +0 -375
  31. lifx_emulator/factories/default_config.py +0 -158
  32. lifx_emulator/factories/factory.py +0 -252
  33. lifx_emulator/factories/firmware_config.py +0 -77
  34. lifx_emulator/factories/serial_generator.py +0 -82
  35. lifx_emulator/handlers/__init__.py +0 -39
  36. lifx_emulator/handlers/base.py +0 -49
  37. lifx_emulator/handlers/device_handlers.py +0 -322
  38. lifx_emulator/handlers/light_handlers.py +0 -503
  39. lifx_emulator/handlers/multizone_handlers.py +0 -249
  40. lifx_emulator/handlers/registry.py +0 -110
  41. lifx_emulator/handlers/tile_handlers.py +0 -488
  42. lifx_emulator/products/__init__.py +0 -28
  43. lifx_emulator/products/generator.py +0 -1079
  44. lifx_emulator/products/registry.py +0 -1530
  45. lifx_emulator/products/specs.py +0 -284
  46. lifx_emulator/products/specs.yml +0 -386
  47. lifx_emulator/protocol/__init__.py +0 -1
  48. lifx_emulator/protocol/base.py +0 -446
  49. lifx_emulator/protocol/const.py +0 -8
  50. lifx_emulator/protocol/generator.py +0 -1384
  51. lifx_emulator/protocol/header.py +0 -159
  52. lifx_emulator/protocol/packets.py +0 -1351
  53. lifx_emulator/protocol/protocol_types.py +0 -817
  54. lifx_emulator/protocol/serializer.py +0 -379
  55. lifx_emulator/repositories/__init__.py +0 -22
  56. lifx_emulator/repositories/device_repository.py +0 -155
  57. lifx_emulator/repositories/storage_backend.py +0 -107
  58. lifx_emulator/scenarios/__init__.py +0 -22
  59. lifx_emulator/scenarios/manager.py +0 -322
  60. lifx_emulator/scenarios/models.py +0 -112
  61. lifx_emulator/scenarios/persistence.py +0 -241
  62. lifx_emulator/server.py +0 -464
  63. lifx_emulator-2.4.0.dist-info/METADATA +0 -107
  64. lifx_emulator-2.4.0.dist-info/RECORD +0 -62
  65. lifx_emulator-2.4.0.dist-info/entry_points.txt +0 -2
  66. lifx_emulator-2.4.0.dist-info/licenses/LICENSE +0 -35
  67. {lifx_emulator-2.4.0.dist-info → lifx_emulator-3.0.1.dist-info}/WHEEL +0 -0
  68. {lifx_emulator → lifx_emulator_app}/api/templates/dashboard.html +0 -0
@@ -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
- ]