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,488 +0,0 @@
1
- """Tile 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 Tile
10
- from lifx_emulator.protocol.protocol_types import (
11
- DeviceStateHostFirmware,
12
- DeviceStateVersion,
13
- LightHsbk,
14
- TileAccelMeas,
15
- TileBufferRect,
16
- TileEffectParameter,
17
- TileEffectSettings,
18
- TileEffectType,
19
- TileStateDevice,
20
- )
21
-
22
- if TYPE_CHECKING:
23
- from lifx_emulator.devices import DeviceState
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- class GetDeviceChainHandler(PacketHandler):
29
- """Handle TileGetDeviceChain (701) -> StateDeviceChain (702)."""
30
-
31
- PKT_TYPE = Tile.GetDeviceChain.PKT_TYPE
32
-
33
- def handle(
34
- self, device_state: DeviceState, packet: Any | None, res_required: bool
35
- ) -> list[Any]:
36
- if not device_state.has_matrix:
37
- return []
38
-
39
- # Build tile device list (max 16 tiles in protocol)
40
- tile_devices = []
41
- for tile in device_state.tile_devices[:16]:
42
- accel_meas = TileAccelMeas(
43
- x=tile["accel_meas_x"], y=tile["accel_meas_y"], z=tile["accel_meas_z"]
44
- )
45
- device_version = DeviceStateVersion(
46
- vendor=tile["device_version_vendor"],
47
- product=tile["device_version_product"],
48
- )
49
- firmware = DeviceStateHostFirmware(
50
- build=tile["firmware_build"],
51
- version_minor=tile["firmware_version_minor"],
52
- version_major=tile["firmware_version_major"],
53
- )
54
- tile_device = TileStateDevice(
55
- accel_meas=accel_meas,
56
- user_x=tile["user_x"],
57
- user_y=tile["user_y"],
58
- width=tile["width"],
59
- height=tile["height"],
60
- device_version=device_version,
61
- firmware=firmware,
62
- )
63
- tile_devices.append(tile_device)
64
-
65
- # Pad to 16 tiles
66
- while len(tile_devices) < 16:
67
- dummy_accel = TileAccelMeas(x=0, y=0, z=0)
68
- dummy_version = DeviceStateVersion(vendor=0, product=0)
69
- dummy_firmware = DeviceStateHostFirmware(
70
- build=0, version_minor=0, version_major=0
71
- )
72
- dummy_tile = TileStateDevice(
73
- accel_meas=dummy_accel,
74
- user_x=0.0,
75
- user_y=0.0,
76
- width=0,
77
- height=0,
78
- device_version=dummy_version,
79
- firmware=dummy_firmware,
80
- )
81
- tile_devices.append(dummy_tile)
82
-
83
- return [
84
- Tile.StateDeviceChain(
85
- start_index=0,
86
- tile_devices=tile_devices,
87
- tile_devices_count=len(device_state.tile_devices),
88
- )
89
- ]
90
-
91
-
92
- class SetUserPositionHandler(PacketHandler):
93
- """Handle TileSetUserPosition (703) - update tile position metadata."""
94
-
95
- PKT_TYPE = Tile.SetUserPosition.PKT_TYPE
96
-
97
- def handle(
98
- self,
99
- device_state: DeviceState,
100
- packet: Tile.SetUserPosition | None,
101
- res_required: bool,
102
- ) -> list[Any]:
103
- if not device_state.has_matrix or not packet:
104
- return []
105
-
106
- logger.info(
107
- f"Tile user position set: tile_index={packet.tile_index}, "
108
- f"user_x={packet.user_x}, user_y={packet.user_y}"
109
- )
110
-
111
- # Update tile position if we have that tile
112
- if packet.tile_index < len(device_state.tile_devices):
113
- device_state.tile_devices[packet.tile_index]["user_x"] = packet.user_x
114
- device_state.tile_devices[packet.tile_index]["user_y"] = packet.user_y
115
-
116
- # No response packet defined for this in protocol
117
- return []
118
-
119
-
120
- class Get64Handler(PacketHandler):
121
- """Handle TileGet64 (707) -> State64 (711)."""
122
-
123
- PKT_TYPE = Tile.Get64.PKT_TYPE
124
-
125
- def handle(
126
- self, device_state: DeviceState, packet: Tile.Get64 | None, res_required: bool
127
- ) -> list[Any]:
128
- if not device_state.has_matrix or not packet:
129
- return []
130
-
131
- tile_index = packet.tile_index
132
- rect = packet.rect
133
-
134
- if tile_index >= len(device_state.tile_devices):
135
- return []
136
-
137
- tile = device_state.tile_devices[tile_index]
138
- tile_width = tile["width"]
139
- tile_height = tile["height"]
140
-
141
- # Get64 always returns framebuffer 0 (the visible buffer)
142
- # regardless of which fb_index is in the request
143
- tile_colors = tile["colors"]
144
-
145
- # Calculate how many rows fit in 64 zones
146
- rows_to_return = 64 // rect.width if rect.width > 0 else 1
147
- rows_to_return = min(rows_to_return, tile_height - rect.y)
148
-
149
- # Extract colors from the requested rectangle
150
- colors = []
151
- zones_extracted = 0
152
-
153
- for row in range(rows_to_return):
154
- y = rect.y + row
155
- if y >= tile_height:
156
- break
157
-
158
- for col in range(rect.width):
159
- x = rect.x + col
160
- if x >= tile_width or zones_extracted >= 64:
161
- colors.append(
162
- LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500)
163
- )
164
- zones_extracted += 1
165
- continue
166
-
167
- # Calculate zone index in flat color array
168
- zone_idx = y * tile_width + x
169
- if zone_idx < len(tile_colors):
170
- colors.append(tile_colors[zone_idx])
171
- else:
172
- colors.append(
173
- LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500)
174
- )
175
- zones_extracted += 1
176
-
177
- # Pad to exactly 64 colors
178
- while len(colors) < 64:
179
- colors.append(LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500))
180
-
181
- # Return with fb_index forced to 0 (visible buffer)
182
- return_rect = TileBufferRect(
183
- fb_index=0, # Always return FB0
184
- x=rect.x,
185
- y=rect.y,
186
- width=rect.width,
187
- )
188
- return [Tile.State64(tile_index=tile_index, rect=return_rect, colors=colors)]
189
-
190
-
191
- class Set64Handler(PacketHandler):
192
- """Handle TileSet64 (715)."""
193
-
194
- PKT_TYPE = Tile.Set64.PKT_TYPE
195
-
196
- def handle(
197
- self, device_state: DeviceState, packet: Tile.Set64 | None, res_required: bool
198
- ) -> list[Any]:
199
- if not device_state.has_matrix or not packet:
200
- return []
201
-
202
- tile_index = packet.tile_index
203
- fb_index = packet.rect.fb_index
204
-
205
- if tile_index >= len(device_state.tile_devices):
206
- return []
207
-
208
- tile = device_state.tile_devices[tile_index]
209
- tile_width = tile["width"]
210
- tile_height = tile["height"]
211
- rect = packet.rect
212
-
213
- # Determine which framebuffer to update
214
- if fb_index == 0:
215
- # Update visible framebuffer (stored in tile_devices)
216
- target_colors = tile["colors"]
217
- else:
218
- # Update non-visible framebuffer (stored in tile_framebuffers)
219
- if tile_index < len(device_state.tile_framebuffers):
220
- fb_storage = device_state.tile_framebuffers[tile_index]
221
- target_colors = fb_storage.get_framebuffer(
222
- fb_index, tile_width, tile_height
223
- )
224
- else:
225
- logger.warning(f"Tile {tile_index} framebuffer storage not initialized")
226
- return []
227
-
228
- # Update colors in the specified rectangle
229
- # Calculate how many rows fit in 64 zones
230
- rows_to_write = 64 // rect.width if rect.width > 0 else 1
231
- rows_to_write = min(rows_to_write, tile_height - rect.y)
232
-
233
- zones_written = 0
234
- for row in range(rows_to_write):
235
- y = rect.y + row
236
- if y >= tile_height:
237
- break
238
-
239
- for col in range(rect.width):
240
- x = rect.x + col
241
- if x >= tile_width or zones_written >= 64:
242
- zones_written += 1
243
- continue
244
-
245
- # Calculate zone index in flat color array
246
- zone_idx = y * tile_width + x
247
- if zone_idx < len(target_colors) and zones_written < len(packet.colors):
248
- target_colors[zone_idx] = packet.colors[zones_written]
249
- zones_written += 1
250
-
251
- logger.info(
252
- f"Tile {tile_index} FB{fb_index} set {zones_written} colors at "
253
- f"({rect.x},{rect.y}), duration={packet.duration}ms"
254
- )
255
-
256
- # Tiles never return a response to Set64 regardless of res_required
257
- # https://lan.developer.lifx.com/docs/changing-a-device#set64---packet-715
258
- return []
259
-
260
-
261
- class CopyFrameBufferHandler(PacketHandler):
262
- """Handle TileCopyFrameBuffer (716) - copy frame buffer (no-op in emulator)."""
263
-
264
- PKT_TYPE = Tile.CopyFrameBuffer.PKT_TYPE
265
-
266
- def handle(
267
- self, device_state: DeviceState, packet: Any | None, res_required: bool
268
- ) -> list[Any]:
269
- if not device_state.has_matrix or not packet:
270
- return []
271
-
272
- tile_index = packet.tile_index
273
- if tile_index >= len(device_state.tile_devices):
274
- return []
275
-
276
- tile = device_state.tile_devices[tile_index]
277
- tile_width = tile["width"]
278
- tile_height = tile["height"]
279
-
280
- src_fb_index = packet.src_fb_index
281
- dst_fb_index = packet.dst_fb_index
282
-
283
- # Get source framebuffer
284
- if src_fb_index == 0:
285
- src_colors = tile["colors"]
286
- else:
287
- if tile_index < len(device_state.tile_framebuffers):
288
- fb_storage = device_state.tile_framebuffers[tile_index]
289
- src_colors = fb_storage.get_framebuffer(
290
- src_fb_index, tile_width, tile_height
291
- )
292
- else:
293
- logger.warning(f"Tile {tile_index} framebuffer storage not initialized")
294
- return []
295
-
296
- # Get destination framebuffer
297
- if dst_fb_index == 0:
298
- dst_colors = tile["colors"]
299
- else:
300
- if tile_index < len(device_state.tile_framebuffers):
301
- fb_storage = device_state.tile_framebuffers[tile_index]
302
- dst_colors = fb_storage.get_framebuffer(
303
- dst_fb_index, tile_width, tile_height
304
- )
305
- else:
306
- logger.warning(f"Tile {tile_index} framebuffer storage not initialized")
307
- return []
308
-
309
- # Copy the specified rectangle from source to destination
310
- src_x = packet.src_x
311
- src_y = packet.src_y
312
- dst_x = packet.dst_x
313
- dst_y = packet.dst_y
314
- width = packet.width
315
- height = packet.height
316
-
317
- zones_copied = 0
318
- for row in range(height):
319
- src_row = src_y + row
320
- dst_row = dst_y + row
321
-
322
- if src_row >= tile_height or dst_row >= tile_height:
323
- break
324
-
325
- for col in range(width):
326
- src_col = src_x + col
327
- dst_col = dst_x + col
328
-
329
- if src_col >= tile_width or dst_col >= tile_width:
330
- continue
331
-
332
- src_idx = src_row * tile_width + src_col
333
- dst_idx = dst_row * tile_width + dst_col
334
-
335
- if src_idx < len(src_colors) and dst_idx < len(dst_colors):
336
- dst_colors[dst_idx] = src_colors[src_idx]
337
- zones_copied += 1
338
-
339
- logger.info(
340
- f"Tile {tile_index} copied {zones_copied} zones from "
341
- f"FB{src_fb_index}({src_x},{src_y}) to "
342
- f"FB{dst_fb_index}({dst_x},{dst_y}), "
343
- f"size={width}x{height}, duration={packet.duration}ms"
344
- )
345
-
346
- return []
347
-
348
-
349
- class GetEffectHandler(PacketHandler):
350
- """Handle TileGetEffect (718) -> StateTileEffect (720)."""
351
-
352
- PKT_TYPE = Tile.GetEffect.PKT_TYPE
353
-
354
- def handle(
355
- self, device_state: DeviceState, packet: Any | None, res_required: bool
356
- ) -> list[Any]:
357
- if not device_state.has_matrix:
358
- return []
359
-
360
- # Build palette (up to 16 colors)
361
- palette = list(device_state.tile_effect_palette[:16])
362
- while len(palette) < 16:
363
- palette.append(LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500))
364
-
365
- # Create effect settings with Sky parameters
366
- from lifx_emulator.protocol.protocol_types import TileEffectSkyType
367
-
368
- # Use defaults for SKY effect when values are None, otherwise use stored values
369
- # NOTE: Must check for None explicitly, not use 'or', because SUNRISE=0 is falsy
370
- effect_type = TileEffectType(device_state.tile_effect_type)
371
- if effect_type == TileEffectType.SKY:
372
- sky_type = (
373
- device_state.tile_effect_sky_type
374
- if device_state.tile_effect_sky_type is not None
375
- else TileEffectSkyType.CLOUDS
376
- )
377
- cloud_sat_min = (
378
- device_state.tile_effect_cloud_sat_min
379
- if device_state.tile_effect_cloud_sat_min is not None
380
- else 50
381
- )
382
- cloud_sat_max = (
383
- device_state.tile_effect_cloud_sat_max
384
- if device_state.tile_effect_cloud_sat_max is not None
385
- else 180
386
- )
387
- else:
388
- sky_type = device_state.tile_effect_sky_type
389
- cloud_sat_min = device_state.tile_effect_cloud_sat_min
390
- cloud_sat_max = device_state.tile_effect_cloud_sat_max
391
-
392
- parameter = TileEffectParameter(
393
- sky_type=TileEffectSkyType(sky_type),
394
- cloud_saturation_min=cloud_sat_min,
395
- cloud_saturation_max=cloud_sat_max,
396
- )
397
- settings = TileEffectSettings(
398
- instanceid=0,
399
- type=TileEffectType(device_state.tile_effect_type),
400
- speed=device_state.tile_effect_speed * 1000, # convert to milliseconds
401
- duration=0, # infinite
402
- parameter=parameter,
403
- palette_count=min(len(device_state.tile_effect_palette), 16),
404
- palette=palette,
405
- )
406
-
407
- return [Tile.StateEffect(settings=settings)]
408
-
409
-
410
- class SetEffectHandler(PacketHandler):
411
- """Handle TileSetEffect (719) -> StateTileEffect (720)."""
412
-
413
- PKT_TYPE = Tile.SetEffect.PKT_TYPE
414
-
415
- def handle(
416
- self,
417
- device_state: DeviceState,
418
- packet: Tile.SetEffect | None,
419
- res_required: bool,
420
- ) -> list[Any]:
421
- if not device_state.has_matrix:
422
- return []
423
-
424
- if packet:
425
- # Sky effect is only supported on LIFX Ceiling devices (176, 177, 201, 202)
426
- # running firmware 4.4 or higher
427
- if packet.settings.type == TileEffectType.SKY:
428
- ceiling_product_ids = {176, 177, 201, 202}
429
- is_ceiling = device_state.product in ceiling_product_ids
430
-
431
- # Check firmware version >= 4.4
432
- firmware_supported = device_state.version_major > 4 or (
433
- device_state.version_major == 4 and device_state.version_minor >= 4
434
- )
435
-
436
- if not (is_ceiling and firmware_supported):
437
- logger.debug(
438
- f"Ignoring SKY effect request: "
439
- f"product={device_state.product}, "
440
- f"firmware={device_state.version_major}."
441
- f"{device_state.version_minor} "
442
- f"(requires Ceiling product and firmware >= 4.4)"
443
- )
444
- return []
445
-
446
- device_state.tile_effect_type = int(packet.settings.type)
447
- device_state.tile_effect_speed = (
448
- packet.settings.speed // 1000
449
- ) # convert to seconds
450
- device_state.tile_effect_palette = list(
451
- packet.settings.palette[: packet.settings.palette_count]
452
- )
453
- device_state.tile_effect_palette_count = packet.settings.palette_count
454
-
455
- # Save Sky effect parameters
456
- device_state.tile_effect_sky_type = int(packet.settings.parameter.sky_type)
457
- device_state.tile_effect_cloud_sat_min = (
458
- packet.settings.parameter.cloud_saturation_min
459
- )
460
- device_state.tile_effect_cloud_sat_max = (
461
- packet.settings.parameter.cloud_saturation_max
462
- )
463
-
464
- logger.info(
465
- f"Tile effect set: type={packet.settings.type}, "
466
- f"speed={packet.settings.speed}ms, "
467
- f"palette_count={packet.settings.palette_count}, "
468
- f"sky_type={packet.settings.parameter.sky_type}, "
469
- f"cloud_sat=[{packet.settings.parameter.cloud_saturation_min}, "
470
- f"{packet.settings.parameter.cloud_saturation_max}]"
471
- )
472
-
473
- if res_required:
474
- handler = GetEffectHandler()
475
- return handler.handle(device_state, None, res_required)
476
- return []
477
-
478
-
479
- # List of all tile handlers for easy registration
480
- ALL_TILE_HANDLERS = [
481
- GetDeviceChainHandler(),
482
- SetUserPositionHandler(),
483
- Get64Handler(),
484
- Set64Handler(),
485
- CopyFrameBufferHandler(),
486
- GetEffectHandler(),
487
- SetEffectHandler(),
488
- ]
@@ -1,28 +0,0 @@
1
- """LIFX product registry module.
2
-
3
- This module provides product information and capability detection for LIFX devices.
4
-
5
- The product registry is auto-generated from the official LIFX
6
- products.json specification.
7
- To update: run `uv run python -m lifx_emulator.products.generator`
8
- """
9
-
10
- from .registry import (
11
- ProductCapability,
12
- ProductInfo,
13
- ProductRegistry,
14
- TemperatureRange,
15
- get_device_class_name,
16
- get_product,
17
- get_registry,
18
- )
19
-
20
- __all__ = [
21
- "ProductCapability",
22
- "ProductInfo",
23
- "ProductRegistry",
24
- "TemperatureRange",
25
- "get_device_class_name",
26
- "get_product",
27
- "get_registry",
28
- ]