lifx-emulator 2.3.1__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 +13 -5
  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 -339
  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 -377
  29. lifx_emulator/factories/__init__.py +0 -37
  30. lifx_emulator/factories/builder.py +0 -373
  31. lifx_emulator/factories/default_config.py +0 -158
  32. lifx_emulator/factories/factory.py +0 -221
  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 -1037
  44. lifx_emulator/products/registry.py +0 -1496
  45. lifx_emulator/products/specs.py +0 -284
  46. lifx_emulator/products/specs.yml +0 -352
  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.3.1.dist-info/METADATA +0 -107
  64. lifx_emulator-2.3.1.dist-info/RECORD +0 -62
  65. lifx_emulator-2.3.1.dist-info/entry_points.txt +0 -2
  66. lifx_emulator-2.3.1.dist-info/licenses/LICENSE +0 -35
  67. {lifx_emulator-2.3.1.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,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
- ]