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.
- lifx_emulator-3.0.1.dist-info/METADATA +102 -0
- lifx_emulator-3.0.1.dist-info/RECORD +18 -0
- lifx_emulator-3.0.1.dist-info/entry_points.txt +2 -0
- lifx_emulator_app/__init__.py +10 -0
- {lifx_emulator → lifx_emulator_app}/__main__.py +2 -3
- {lifx_emulator → lifx_emulator_app}/api/__init__.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/app.py +3 -3
- {lifx_emulator → lifx_emulator_app}/api/mappers/__init__.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/mappers/device_mapper.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/models.py +1 -2
- lifx_emulator_app/api/routers/__init__.py +11 -0
- {lifx_emulator → lifx_emulator_app}/api/routers/devices.py +2 -2
- {lifx_emulator → lifx_emulator_app}/api/routers/monitoring.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/routers/scenarios.py +1 -1
- lifx_emulator_app/api/services/__init__.py +8 -0
- {lifx_emulator → lifx_emulator_app}/api/services/device_service.py +3 -2
- lifx_emulator/__init__.py +0 -31
- lifx_emulator/api/routers/__init__.py +0 -11
- lifx_emulator/api/services/__init__.py +0 -8
- lifx_emulator/constants.py +0 -33
- lifx_emulator/devices/__init__.py +0 -37
- lifx_emulator/devices/device.py +0 -395
- lifx_emulator/devices/manager.py +0 -256
- lifx_emulator/devices/observers.py +0 -139
- lifx_emulator/devices/persistence.py +0 -308
- lifx_emulator/devices/state_restorer.py +0 -259
- lifx_emulator/devices/state_serializer.py +0 -157
- lifx_emulator/devices/states.py +0 -381
- lifx_emulator/factories/__init__.py +0 -39
- lifx_emulator/factories/builder.py +0 -375
- lifx_emulator/factories/default_config.py +0 -158
- lifx_emulator/factories/factory.py +0 -252
- lifx_emulator/factories/firmware_config.py +0 -77
- lifx_emulator/factories/serial_generator.py +0 -82
- lifx_emulator/handlers/__init__.py +0 -39
- lifx_emulator/handlers/base.py +0 -49
- lifx_emulator/handlers/device_handlers.py +0 -322
- lifx_emulator/handlers/light_handlers.py +0 -503
- lifx_emulator/handlers/multizone_handlers.py +0 -249
- lifx_emulator/handlers/registry.py +0 -110
- lifx_emulator/handlers/tile_handlers.py +0 -488
- lifx_emulator/products/__init__.py +0 -28
- lifx_emulator/products/generator.py +0 -1079
- lifx_emulator/products/registry.py +0 -1530
- lifx_emulator/products/specs.py +0 -284
- lifx_emulator/products/specs.yml +0 -386
- lifx_emulator/protocol/__init__.py +0 -1
- lifx_emulator/protocol/base.py +0 -446
- lifx_emulator/protocol/const.py +0 -8
- lifx_emulator/protocol/generator.py +0 -1384
- lifx_emulator/protocol/header.py +0 -159
- lifx_emulator/protocol/packets.py +0 -1351
- lifx_emulator/protocol/protocol_types.py +0 -817
- lifx_emulator/protocol/serializer.py +0 -379
- lifx_emulator/repositories/__init__.py +0 -22
- lifx_emulator/repositories/device_repository.py +0 -155
- lifx_emulator/repositories/storage_backend.py +0 -107
- lifx_emulator/scenarios/__init__.py +0 -22
- lifx_emulator/scenarios/manager.py +0 -322
- lifx_emulator/scenarios/models.py +0 -112
- lifx_emulator/scenarios/persistence.py +0 -241
- lifx_emulator/server.py +0 -464
- lifx_emulator-2.4.0.dist-info/METADATA +0 -107
- lifx_emulator-2.4.0.dist-info/RECORD +0 -62
- lifx_emulator-2.4.0.dist-info/entry_points.txt +0 -2
- lifx_emulator-2.4.0.dist-info/licenses/LICENSE +0 -35
- {lifx_emulator-2.4.0.dist-info → lifx_emulator-3.0.1.dist-info}/WHEEL +0 -0
- {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
|
-
]
|