lifx-emulator 2.0.0__py3-none-any.whl → 2.2.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.
- lifx_emulator/devices/states.py +14 -1
- lifx_emulator/factories/builder.py +3 -1
- lifx_emulator/factories/firmware_config.py +19 -1
- lifx_emulator/handlers/tile_handlers.py +51 -10
- lifx_emulator/products/generator.py +36 -1
- lifx_emulator/products/specs.py +43 -0
- lifx_emulator/products/specs.yml +30 -5
- lifx_emulator/protocol/base.py +60 -2
- lifx_emulator/protocol/protocol_types.py +35 -62
- lifx_emulator/server.py +4 -2
- {lifx_emulator-2.0.0.dist-info → lifx_emulator-2.2.0.dist-info}/METADATA +1 -1
- {lifx_emulator-2.0.0.dist-info → lifx_emulator-2.2.0.dist-info}/RECORD +15 -15
- {lifx_emulator-2.0.0.dist-info → lifx_emulator-2.2.0.dist-info}/WHEEL +0 -0
- {lifx_emulator-2.0.0.dist-info → lifx_emulator-2.2.0.dist-info}/entry_points.txt +0 -0
- {lifx_emulator-2.0.0.dist-info → lifx_emulator-2.2.0.dist-info}/licenses/LICENSE +0 -0
lifx_emulator/devices/states.py
CHANGED
|
@@ -90,10 +90,17 @@ class MatrixState:
|
|
|
90
90
|
tile_devices: list[dict[str, Any]]
|
|
91
91
|
tile_width: int
|
|
92
92
|
tile_height: int
|
|
93
|
-
effect_type: int = 0 # 0=OFF, 2=MORPH, 3=FLAME
|
|
93
|
+
effect_type: int = 0 # 0=OFF, 2=MORPH, 3=FLAME, 5=SKY
|
|
94
94
|
effect_speed: int = 5 # Duration of one cycle in seconds
|
|
95
95
|
effect_palette_count: int = 0
|
|
96
96
|
effect_palette: list[LightHsbk] = field(default_factory=list)
|
|
97
|
+
effect_sky_type: int = 0 # 0=SUNRISE, 1=SUNSET, 2=CLOUDS (only when effect_type=5)
|
|
98
|
+
effect_cloud_sat_min: int = (
|
|
99
|
+
0 # Min cloud saturation 0-200 (only when effect_type=5)
|
|
100
|
+
)
|
|
101
|
+
effect_cloud_sat_max: int = (
|
|
102
|
+
0 # Max cloud saturation 0-200 (only when effect_type=5)
|
|
103
|
+
)
|
|
97
104
|
|
|
98
105
|
|
|
99
106
|
@dataclass
|
|
@@ -205,6 +212,9 @@ class DeviceState:
|
|
|
205
212
|
"tile_effect_speed": ("matrix", "effect_speed"),
|
|
206
213
|
"tile_effect_palette_count": ("matrix", "effect_palette_count"),
|
|
207
214
|
"tile_effect_palette": ("matrix", "effect_palette"),
|
|
215
|
+
"tile_effect_sky_type": ("matrix", "effect_sky_type"),
|
|
216
|
+
"tile_effect_cloud_sat_min": ("matrix", "effect_cloud_sat_min"),
|
|
217
|
+
"tile_effect_cloud_sat_max": ("matrix", "effect_cloud_sat_max"),
|
|
208
218
|
}
|
|
209
219
|
|
|
210
220
|
# Default values for optional state attributes when state object is None
|
|
@@ -227,6 +237,9 @@ class DeviceState:
|
|
|
227
237
|
"tile_effect_speed": 0,
|
|
228
238
|
"tile_effect_palette_count": 0,
|
|
229
239
|
"tile_effect_palette": [],
|
|
240
|
+
"tile_effect_sky_type": 0,
|
|
241
|
+
"tile_effect_cloud_sat_min": 0,
|
|
242
|
+
"tile_effect_cloud_sat_max": 0,
|
|
230
243
|
}
|
|
231
244
|
|
|
232
245
|
def get_target_bytes(self) -> bytes:
|
|
@@ -211,7 +211,9 @@ class DeviceBuilder:
|
|
|
211
211
|
|
|
212
212
|
# 3. Determine firmware version
|
|
213
213
|
version_major, version_minor = self._firmware_config.get_firmware_version(
|
|
214
|
-
|
|
214
|
+
product_id=self._product_info.pid,
|
|
215
|
+
extended_multizone=self._extended_multizone,
|
|
216
|
+
override=self._firmware_version,
|
|
215
217
|
)
|
|
216
218
|
|
|
217
219
|
# 4. Get default color
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from lifx_emulator.products.specs import get_default_firmware_version
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
class FirmwareConfig:
|
|
7
9
|
"""Determines firmware versions for devices.
|
|
@@ -25,12 +27,19 @@ class FirmwareConfig:
|
|
|
25
27
|
|
|
26
28
|
def get_firmware_version(
|
|
27
29
|
self,
|
|
30
|
+
product_id: int | None = None,
|
|
28
31
|
extended_multizone: bool | None = None,
|
|
29
32
|
override: tuple[int, int] | None = None,
|
|
30
33
|
) -> tuple[int, int]:
|
|
31
|
-
"""Get firmware version based on extended multizone support.
|
|
34
|
+
"""Get firmware version based on product specs or extended multizone support.
|
|
35
|
+
|
|
36
|
+
Precedence order:
|
|
37
|
+
1. Explicit override parameter
|
|
38
|
+
2. Product-specific default from specs.yml
|
|
39
|
+
3. Extended multizone flag (3.70 for True/None, 2.60 for False)
|
|
32
40
|
|
|
33
41
|
Args:
|
|
42
|
+
product_id: Optional product ID to check specs for defaults
|
|
34
43
|
extended_multizone: Whether device supports extended multizone.
|
|
35
44
|
None or True defaults to 3.70, False gives 2.60
|
|
36
45
|
override: Optional explicit firmware version to use
|
|
@@ -46,11 +55,20 @@ class FirmwareConfig:
|
|
|
46
55
|
(2, 60)
|
|
47
56
|
>>> config.get_firmware_version(override=(4, 0))
|
|
48
57
|
(4, 0)
|
|
58
|
+
>>> # With product_id, uses specs if defined
|
|
59
|
+
>>> config.get_firmware_version(product_id=27) # doctest: +SKIP
|
|
60
|
+
(3, 70)
|
|
49
61
|
"""
|
|
50
62
|
# Explicit override takes precedence
|
|
51
63
|
if override is not None:
|
|
52
64
|
return override
|
|
53
65
|
|
|
66
|
+
# Check product-specific defaults from specs
|
|
67
|
+
if product_id is not None:
|
|
68
|
+
specs_version = get_default_firmware_version(product_id)
|
|
69
|
+
if specs_version is not None:
|
|
70
|
+
return specs_version
|
|
71
|
+
|
|
54
72
|
# None or True defaults to extended (3.70)
|
|
55
73
|
# Only explicit False gives legacy (2.60)
|
|
56
74
|
if extended_multizone is False:
|
|
@@ -237,16 +237,24 @@ class GetEffectHandler(PacketHandler):
|
|
|
237
237
|
while len(palette) < 16:
|
|
238
238
|
palette.append(LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500))
|
|
239
239
|
|
|
240
|
-
# Create effect settings
|
|
240
|
+
# Create effect settings with Sky parameters
|
|
241
|
+
from lifx_emulator.protocol.protocol_types import TileEffectSkyType
|
|
242
|
+
|
|
243
|
+
# Use defaults for SKY effect (type=5), otherwise use stored values
|
|
244
|
+
effect_type = TileEffectType(device_state.tile_effect_type)
|
|
245
|
+
if effect_type == TileEffectType.SKY:
|
|
246
|
+
sky_type = device_state.tile_effect_sky_type or TileEffectSkyType.CLOUDS
|
|
247
|
+
cloud_sat_min = device_state.tile_effect_cloud_sat_min or 50
|
|
248
|
+
cloud_sat_max = device_state.tile_effect_cloud_sat_max or 180
|
|
249
|
+
else:
|
|
250
|
+
sky_type = device_state.tile_effect_sky_type
|
|
251
|
+
cloud_sat_min = device_state.tile_effect_cloud_sat_min
|
|
252
|
+
cloud_sat_max = device_state.tile_effect_cloud_sat_max
|
|
253
|
+
|
|
241
254
|
parameter = TileEffectParameter(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
parameter3=0,
|
|
246
|
-
parameter4=0,
|
|
247
|
-
parameter5=0,
|
|
248
|
-
parameter6=0,
|
|
249
|
-
parameter7=0,
|
|
255
|
+
sky_type=TileEffectSkyType(sky_type),
|
|
256
|
+
cloud_saturation_min=cloud_sat_min,
|
|
257
|
+
cloud_saturation_max=cloud_sat_max,
|
|
250
258
|
)
|
|
251
259
|
settings = TileEffectSettings(
|
|
252
260
|
instanceid=0,
|
|
@@ -276,6 +284,27 @@ class SetEffectHandler(PacketHandler):
|
|
|
276
284
|
return []
|
|
277
285
|
|
|
278
286
|
if packet:
|
|
287
|
+
# Sky effect is only supported on LIFX Ceiling devices (176, 177, 201, 202)
|
|
288
|
+
# running firmware 4.4 or higher
|
|
289
|
+
if packet.settings.type == TileEffectType.SKY:
|
|
290
|
+
ceiling_product_ids = {176, 177, 201, 202}
|
|
291
|
+
is_ceiling = device_state.product in ceiling_product_ids
|
|
292
|
+
|
|
293
|
+
# Check firmware version >= 4.4
|
|
294
|
+
firmware_supported = device_state.version_major > 4 or (
|
|
295
|
+
device_state.version_major == 4 and device_state.version_minor >= 4
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if not (is_ceiling and firmware_supported):
|
|
299
|
+
logger.debug(
|
|
300
|
+
f"Ignoring SKY effect request: "
|
|
301
|
+
f"product={device_state.product}, "
|
|
302
|
+
f"firmware={device_state.version_major}."
|
|
303
|
+
f"{device_state.version_minor} "
|
|
304
|
+
f"(requires Ceiling product and firmware >= 4.4)"
|
|
305
|
+
)
|
|
306
|
+
return []
|
|
307
|
+
|
|
279
308
|
device_state.tile_effect_type = int(packet.settings.type)
|
|
280
309
|
device_state.tile_effect_speed = (
|
|
281
310
|
packet.settings.speed // 1000
|
|
@@ -285,10 +314,22 @@ class SetEffectHandler(PacketHandler):
|
|
|
285
314
|
)
|
|
286
315
|
device_state.tile_effect_palette_count = packet.settings.palette_count
|
|
287
316
|
|
|
317
|
+
# Save Sky effect parameters
|
|
318
|
+
device_state.tile_effect_sky_type = int(packet.settings.parameter.sky_type)
|
|
319
|
+
device_state.tile_effect_cloud_sat_min = (
|
|
320
|
+
packet.settings.parameter.cloud_saturation_min
|
|
321
|
+
)
|
|
322
|
+
device_state.tile_effect_cloud_sat_max = (
|
|
323
|
+
packet.settings.parameter.cloud_saturation_max
|
|
324
|
+
)
|
|
325
|
+
|
|
288
326
|
logger.info(
|
|
289
327
|
f"Tile effect set: type={packet.settings.type}, "
|
|
290
328
|
f"speed={packet.settings.speed}ms, "
|
|
291
|
-
f"palette_count={packet.settings.palette_count}"
|
|
329
|
+
f"palette_count={packet.settings.palette_count}, "
|
|
330
|
+
f"sky_type={packet.settings.parameter.sky_type}, "
|
|
331
|
+
f"cloud_sat=[{packet.settings.parameter.cloud_saturation_min}, "
|
|
332
|
+
f"{packet.settings.parameter.cloud_saturation_max}]"
|
|
292
333
|
)
|
|
293
334
|
|
|
294
335
|
if res_required:
|
|
@@ -788,7 +788,7 @@ def _generate_yaml_header() -> list[str]:
|
|
|
788
788
|
"#",
|
|
789
789
|
"# This file contains product-specific details that are not available in the",
|
|
790
790
|
"# upstream LIFX products.json catalog, such as default zone counts, tile",
|
|
791
|
-
"# configurations, and other device-specific defaults.",
|
|
791
|
+
"# configurations, firmware versions, and other device-specific defaults.",
|
|
792
792
|
"#",
|
|
793
793
|
"# These values are used by the emulator to create realistic device",
|
|
794
794
|
"# configurations when specific parameters are not provided by the user.",
|
|
@@ -809,8 +809,25 @@ def _generate_yaml_header() -> list[str]:
|
|
|
809
809
|
"# tile_width: <number> # Width of each tile in pixels",
|
|
810
810
|
"# tile_height: <number> # Height of each tile in pixels",
|
|
811
811
|
"#",
|
|
812
|
+
"# # Host firmware version (optional, overrides auto firmware selection)",
|
|
813
|
+
"# default_firmware_major: <number> # Firmware major version (e.g., 3)",
|
|
814
|
+
"# default_firmware_minor: <number> # Firmware minor version (e.g., 70)",
|
|
815
|
+
"#",
|
|
812
816
|
"# # Other device-specific defaults",
|
|
813
817
|
'# notes: "<string>" # Notes about product',
|
|
818
|
+
"#",
|
|
819
|
+
"# Firmware Version Notes:",
|
|
820
|
+
"# ----------------------",
|
|
821
|
+
"# If default_firmware_major and default_firmware_minor are both specified ",
|
|
822
|
+
"# they will be used as the default firmware version when creating devices",
|
|
823
|
+
"# of that type. This overrides the automatic firmware version selection",
|
|
824
|
+
"# based on extended_multizone capability (which defaults to 3.70 for extended",
|
|
825
|
+
"# multizone or 2.60 for non-extended).",
|
|
826
|
+
"#",
|
|
827
|
+
"# Precedence order for firmware version:",
|
|
828
|
+
"# 1. Explicit firmware_version parameter to create_device()",
|
|
829
|
+
"# 2. Product-specific default from this specs.yml file",
|
|
830
|
+
"# 3. Automatic selection based on extended_multizone flag",
|
|
814
831
|
"",
|
|
815
832
|
"products:",
|
|
816
833
|
]
|
|
@@ -847,6 +864,15 @@ def _generate_multizone_section(
|
|
|
847
864
|
lines.append(f" min_zone_count: {specs['min_zone_count']}")
|
|
848
865
|
lines.append(f" max_zone_count: {specs['max_zone_count']}")
|
|
849
866
|
|
|
867
|
+
# Add firmware version if present
|
|
868
|
+
if "default_firmware_major" in specs and "default_firmware_minor" in specs:
|
|
869
|
+
lines.append(
|
|
870
|
+
f" default_firmware_major: {specs['default_firmware_major']}"
|
|
871
|
+
)
|
|
872
|
+
lines.append(
|
|
873
|
+
f" default_firmware_minor: {specs['default_firmware_minor']}"
|
|
874
|
+
)
|
|
875
|
+
|
|
850
876
|
notes = specs.get("notes", "")
|
|
851
877
|
if notes:
|
|
852
878
|
notes_escaped = notes.replace('"', '\\"')
|
|
@@ -889,6 +915,15 @@ def _generate_matrix_section(
|
|
|
889
915
|
lines.append(f" tile_width: {specs['tile_width']}")
|
|
890
916
|
lines.append(f" tile_height: {specs['tile_height']}")
|
|
891
917
|
|
|
918
|
+
# Add firmware version if present
|
|
919
|
+
if "default_firmware_major" in specs and "default_firmware_minor" in specs:
|
|
920
|
+
lines.append(
|
|
921
|
+
f" default_firmware_major: {specs['default_firmware_major']}"
|
|
922
|
+
)
|
|
923
|
+
lines.append(
|
|
924
|
+
f" default_firmware_minor: {specs['default_firmware_minor']}"
|
|
925
|
+
)
|
|
926
|
+
|
|
892
927
|
notes = specs.get("notes", "")
|
|
893
928
|
if notes:
|
|
894
929
|
notes_escaped = notes.replace('"', '\\"')
|
lifx_emulator/products/specs.py
CHANGED
|
@@ -27,6 +27,8 @@ class ProductSpecs:
|
|
|
27
27
|
max_tile_count: Maximum tiles supported
|
|
28
28
|
tile_width: Width of each tile in pixels
|
|
29
29
|
tile_height: Height of each tile in pixels
|
|
30
|
+
default_firmware_major: Default firmware major version
|
|
31
|
+
default_firmware_minor: Default firmware minor version
|
|
30
32
|
notes: Human-readable notes about this product
|
|
31
33
|
"""
|
|
32
34
|
|
|
@@ -39,6 +41,8 @@ class ProductSpecs:
|
|
|
39
41
|
max_tile_count: int | None = None
|
|
40
42
|
tile_width: int | None = None
|
|
41
43
|
tile_height: int | None = None
|
|
44
|
+
default_firmware_major: int | None = None
|
|
45
|
+
default_firmware_minor: int | None = None
|
|
42
46
|
notes: str | None = None
|
|
43
47
|
|
|
44
48
|
@property
|
|
@@ -51,6 +55,14 @@ class ProductSpecs:
|
|
|
51
55
|
"""Check if this product has matrix-specific specs."""
|
|
52
56
|
return self.tile_width is not None or self.tile_height is not None
|
|
53
57
|
|
|
58
|
+
@property
|
|
59
|
+
def has_firmware_specs(self) -> bool:
|
|
60
|
+
"""Check if this product has firmware version specs."""
|
|
61
|
+
return (
|
|
62
|
+
self.default_firmware_major is not None
|
|
63
|
+
and self.default_firmware_minor is not None
|
|
64
|
+
)
|
|
65
|
+
|
|
54
66
|
|
|
55
67
|
class SpecsRegistry:
|
|
56
68
|
"""Registry of product specs loaded from specs.yml."""
|
|
@@ -93,6 +105,8 @@ class SpecsRegistry:
|
|
|
93
105
|
max_tile_count=specs_data.get("max_tile_count"),
|
|
94
106
|
tile_width=specs_data.get("tile_width"),
|
|
95
107
|
tile_height=specs_data.get("tile_height"),
|
|
108
|
+
default_firmware_major=specs_data.get("default_firmware_major"),
|
|
109
|
+
default_firmware_minor=specs_data.get("default_firmware_minor"),
|
|
96
110
|
notes=specs_data.get("notes"),
|
|
97
111
|
)
|
|
98
112
|
|
|
@@ -169,6 +183,23 @@ class SpecsRegistry:
|
|
|
169
183
|
return (specs.tile_width, specs.tile_height)
|
|
170
184
|
return None
|
|
171
185
|
|
|
186
|
+
def get_default_firmware_version(self, product_id: int) -> tuple[int, int] | None:
|
|
187
|
+
"""Get default firmware version for a product.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
product_id: Product ID
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Tuple of (major, minor) if defined, None otherwise
|
|
194
|
+
"""
|
|
195
|
+
specs = self.get_specs(product_id)
|
|
196
|
+
if specs and specs.has_firmware_specs:
|
|
197
|
+
# has_firmware_specs ensures both values are not None
|
|
198
|
+
assert specs.default_firmware_major is not None # nosec
|
|
199
|
+
assert specs.default_firmware_minor is not None # nosec
|
|
200
|
+
return (specs.default_firmware_major, specs.default_firmware_minor)
|
|
201
|
+
return None
|
|
202
|
+
|
|
172
203
|
def __len__(self) -> int:
|
|
173
204
|
"""Get number of products with specs."""
|
|
174
205
|
if not self._loaded:
|
|
@@ -239,3 +270,15 @@ def get_tile_dimensions(product_id: int) -> tuple[int, int] | None:
|
|
|
239
270
|
Tuple of (width, height) if defined, None otherwise
|
|
240
271
|
"""
|
|
241
272
|
return _specs_registry.get_tile_dimensions(product_id)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def get_default_firmware_version(product_id: int) -> tuple[int, int] | None:
|
|
276
|
+
"""Get default firmware version for a product.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
product_id: Product ID
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Tuple of (major, minor) if defined, None otherwise
|
|
283
|
+
"""
|
|
284
|
+
return _specs_registry.get_default_firmware_version(product_id)
|
lifx_emulator/products/specs.yml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# This file contains product-specific details that are not available in the
|
|
5
5
|
# upstream LIFX products.json catalog, such as default zone counts, tile
|
|
6
|
-
# configurations, and other device-specific defaults.
|
|
6
|
+
# configurations, firmware versions, and other device-specific defaults.
|
|
7
7
|
#
|
|
8
8
|
# These values are used by the emulator to create realistic device
|
|
9
9
|
# configurations when specific parameters are not provided by the user.
|
|
@@ -24,8 +24,25 @@
|
|
|
24
24
|
# tile_width: <number> # Width of each tile in pixels
|
|
25
25
|
# tile_height: <number> # Height of each tile in pixels
|
|
26
26
|
#
|
|
27
|
+
# # Host firmware version (optional, overrides automatic firmware selection)
|
|
28
|
+
# default_firmware_major: <number> # Firmware major version (e.g., 3)
|
|
29
|
+
# default_firmware_minor: <number> # Firmware minor version (e.g., 70)
|
|
30
|
+
#
|
|
27
31
|
# # Other device-specific defaults
|
|
28
32
|
# notes: "<string>" # Notes about product
|
|
33
|
+
#
|
|
34
|
+
# Firmware Version Notes:
|
|
35
|
+
# ----------------------
|
|
36
|
+
# If default_firmware_major and default_firmware_minor are both specified for a
|
|
37
|
+
# product, they will be used as the default firmware version when creating devices
|
|
38
|
+
# of that product type. This overrides the automatic firmware version selection
|
|
39
|
+
# based on extended_multizone capability (which defaults to 3.70 for extended
|
|
40
|
+
# multizone or 2.60 for non-extended).
|
|
41
|
+
#
|
|
42
|
+
# Precedence order for firmware version:
|
|
43
|
+
# 1. Explicit firmware_version parameter to create_device()
|
|
44
|
+
# 2. Product-specific default from this specs.yml file
|
|
45
|
+
# 3. Automatic selection based on extended_multizone flag
|
|
29
46
|
|
|
30
47
|
products:
|
|
31
48
|
# ========================================
|
|
@@ -220,15 +237,19 @@ products:
|
|
|
220
237
|
max_tile_count: 1
|
|
221
238
|
tile_width: 8
|
|
222
239
|
tile_height: 8
|
|
240
|
+
default_firmware_major: 4
|
|
241
|
+
default_firmware_minor: 10
|
|
223
242
|
notes: 'LIFX Ceiling, 8x8 matrix, zones 1-63: downlight, zone 64: uplight'
|
|
224
243
|
|
|
225
|
-
177: # LIFX
|
|
244
|
+
177: # LIFX Ceiling Intl
|
|
226
245
|
default_tile_count: 1
|
|
227
246
|
min_tile_count: 1
|
|
228
247
|
max_tile_count: 1
|
|
229
|
-
tile_width:
|
|
230
|
-
tile_height:
|
|
231
|
-
|
|
248
|
+
tile_width: 8
|
|
249
|
+
tile_height: 8
|
|
250
|
+
default_firmware_major: 4
|
|
251
|
+
default_firmware_minor: 10
|
|
252
|
+
notes: LIFX Ceiling Intl
|
|
232
253
|
|
|
233
254
|
185: # LIFX Candle Color US
|
|
234
255
|
default_tile_count: 1
|
|
@@ -252,6 +273,8 @@ products:
|
|
|
252
273
|
max_tile_count: 1
|
|
253
274
|
tile_width: 16
|
|
254
275
|
tile_height: 8
|
|
276
|
+
default_firmware_major: 4
|
|
277
|
+
default_firmware_minor: 10
|
|
255
278
|
notes: LIFX Ceiling 13x26" US
|
|
256
279
|
|
|
257
280
|
202: # LIFX Ceiling 13x26" Intl
|
|
@@ -260,6 +283,8 @@ products:
|
|
|
260
283
|
max_tile_count: 1
|
|
261
284
|
tile_width: 16
|
|
262
285
|
tile_height: 8
|
|
286
|
+
default_firmware_major: 4
|
|
287
|
+
default_firmware_minor: 10
|
|
263
288
|
notes: LIFX Ceiling 13x26" Intl
|
|
264
289
|
|
|
265
290
|
215: # LIFX Candle Color US
|
lifx_emulator/protocol/base.py
CHANGED
|
@@ -10,10 +10,13 @@ Performance optimizations:
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import logging
|
|
13
14
|
import re
|
|
14
|
-
from dataclasses import dataclass
|
|
15
|
+
from dataclasses import asdict, dataclass
|
|
15
16
|
from typing import Any, ClassVar
|
|
16
17
|
|
|
18
|
+
_LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
17
20
|
# Performance optimization: Pre-compiled regex patterns
|
|
18
21
|
_CAMEL_TO_SNAKE_PATTERN = re.compile(r"(?<!^)(?=[A-Z])")
|
|
19
22
|
_ARRAY_TYPE_PATTERN = re.compile(r"\[(\d+)\](.+)")
|
|
@@ -39,6 +42,11 @@ class Packet:
|
|
|
39
42
|
_fields: ClassVar[list[dict[str, Any]]]
|
|
40
43
|
_field_info: ClassVar[list[tuple[str, str, int]] | None] = None
|
|
41
44
|
|
|
45
|
+
@property
|
|
46
|
+
def as_dict(self) -> dict[str, Any]:
|
|
47
|
+
"""Return packet as dictionary."""
|
|
48
|
+
return asdict(self)
|
|
49
|
+
|
|
42
50
|
def pack(self) -> bytes:
|
|
43
51
|
"""Pack packet to bytes using field metadata.
|
|
44
52
|
|
|
@@ -76,9 +84,25 @@ class Packet:
|
|
|
76
84
|
offset: Offset in bytes to start unpacking
|
|
77
85
|
|
|
78
86
|
Returns:
|
|
79
|
-
Packet instance
|
|
87
|
+
Packet instance with label fields decoded to strings
|
|
80
88
|
"""
|
|
81
89
|
packet, _ = cls._unpack_internal(data, offset)
|
|
90
|
+
|
|
91
|
+
# Decode label fields from bytes to string in-place
|
|
92
|
+
# This ensures all State packets have human-readable labels
|
|
93
|
+
cls._decode_labels_inplace(packet)
|
|
94
|
+
|
|
95
|
+
# Log packet values after unpacking and decoding labels
|
|
96
|
+
packet_values = asdict(packet)
|
|
97
|
+
_LOGGER.debug(
|
|
98
|
+
{
|
|
99
|
+
"class": "Packet",
|
|
100
|
+
"method": "unpack",
|
|
101
|
+
"packet_type": type(packet).__name__,
|
|
102
|
+
"values": packet_values,
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
82
106
|
return packet
|
|
83
107
|
|
|
84
108
|
@classmethod
|
|
@@ -177,6 +201,9 @@ class Packet:
|
|
|
177
201
|
"MultiZoneApplicationRequest",
|
|
178
202
|
"MultiZoneEffectType",
|
|
179
203
|
"MultiZoneExtendedApplicationRequest",
|
|
204
|
+
"TileEffectSkyPalette",
|
|
205
|
+
"TileEffectSkyType",
|
|
206
|
+
"TileEffectType",
|
|
180
207
|
}
|
|
181
208
|
is_enum = is_nested and base_type in enum_types
|
|
182
209
|
|
|
@@ -320,6 +347,9 @@ class Packet:
|
|
|
320
347
|
MultiZoneApplicationRequest,
|
|
321
348
|
MultiZoneEffectType,
|
|
322
349
|
MultiZoneExtendedApplicationRequest,
|
|
350
|
+
TileEffectSkyPalette,
|
|
351
|
+
TileEffectSkyType,
|
|
352
|
+
TileEffectType,
|
|
323
353
|
)
|
|
324
354
|
|
|
325
355
|
base_type, array_count, is_nested = cls._parse_field_type(field_type)
|
|
@@ -331,6 +361,9 @@ class Packet:
|
|
|
331
361
|
"MultiZoneApplicationRequest": MultiZoneApplicationRequest,
|
|
332
362
|
"MultiZoneEffectType": MultiZoneEffectType,
|
|
333
363
|
"MultiZoneExtendedApplicationRequest": MultiZoneExtendedApplicationRequest,
|
|
364
|
+
"TileEffectSkyPalette": TileEffectSkyPalette,
|
|
365
|
+
"TileEffectSkyType": TileEffectSkyType,
|
|
366
|
+
"TileEffectType": TileEffectType,
|
|
334
367
|
}
|
|
335
368
|
|
|
336
369
|
if array_count:
|
|
@@ -386,3 +419,28 @@ class Packet:
|
|
|
386
419
|
# Cache the result
|
|
387
420
|
_FIELD_TYPE_CACHE[field_type] = result
|
|
388
421
|
return result
|
|
422
|
+
|
|
423
|
+
@staticmethod
|
|
424
|
+
def _decode_labels_inplace(packet: object) -> None:
|
|
425
|
+
"""Decode label fields from bytes to string in-place.
|
|
426
|
+
|
|
427
|
+
Automatically finds and decodes any field named 'label' or ending with '_label'
|
|
428
|
+
for all State packets. This ensures human-readable labels in all contexts.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
packet: Packet instance to process (modified in-place)
|
|
432
|
+
"""
|
|
433
|
+
from dataclasses import fields, is_dataclass
|
|
434
|
+
|
|
435
|
+
if not is_dataclass(packet):
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
for field_info in fields(packet):
|
|
439
|
+
# Check if this looks like a label field
|
|
440
|
+
if field_info.name == "label" or field_info.name.endswith("_label"):
|
|
441
|
+
value = getattr(packet, field_info.name)
|
|
442
|
+
if isinstance(value, bytes):
|
|
443
|
+
# Decode: strip null terminator, decode UTF-8
|
|
444
|
+
decoded = value.rstrip(b"\x00").decode("utf-8")
|
|
445
|
+
# Use object.__setattr__ to bypass frozen dataclass if needed
|
|
446
|
+
object.__setattr__(packet, field_info.name, decoded)
|
|
@@ -505,16 +505,11 @@ class TileBufferRect:
|
|
|
505
505
|
|
|
506
506
|
@dataclass
|
|
507
507
|
class TileEffectParameter:
|
|
508
|
-
"""Auto-generated field structure."""
|
|
508
|
+
"""Auto-generated field structure for Sky effects."""
|
|
509
509
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
parameter3: int
|
|
514
|
-
parameter4: int
|
|
515
|
-
parameter5: int
|
|
516
|
-
parameter6: int
|
|
517
|
-
parameter7: int
|
|
510
|
+
sky_type: TileEffectSkyType
|
|
511
|
+
cloud_saturation_min: int
|
|
512
|
+
cloud_saturation_max: int
|
|
518
513
|
|
|
519
514
|
def pack(self) -> bytes:
|
|
520
515
|
"""Pack to bytes."""
|
|
@@ -522,22 +517,18 @@ class TileEffectParameter:
|
|
|
522
517
|
|
|
523
518
|
result = b""
|
|
524
519
|
|
|
525
|
-
#
|
|
526
|
-
result += serializer.pack_value(self.
|
|
527
|
-
#
|
|
528
|
-
result += serializer.
|
|
529
|
-
#
|
|
530
|
-
result += serializer.pack_value(self.
|
|
531
|
-
#
|
|
532
|
-
result += serializer.
|
|
533
|
-
#
|
|
534
|
-
result += serializer.pack_value(self.
|
|
535
|
-
#
|
|
536
|
-
result += serializer.
|
|
537
|
-
# parameter6: uint32
|
|
538
|
-
result += serializer.pack_value(self.parameter6, "uint32")
|
|
539
|
-
# parameter7: uint32
|
|
540
|
-
result += serializer.pack_value(self.parameter7, "uint32")
|
|
520
|
+
# sky_type: TileEffectSkyType (enum)
|
|
521
|
+
result += serializer.pack_value(int(self.sky_type), "uint8")
|
|
522
|
+
# Reserved 3 bytes
|
|
523
|
+
result += serializer.pack_reserved(3)
|
|
524
|
+
# cloud_saturation_min: uint8
|
|
525
|
+
result += serializer.pack_value(self.cloud_saturation_min, "uint8")
|
|
526
|
+
# Reserved 3 bytes
|
|
527
|
+
result += serializer.pack_reserved(3)
|
|
528
|
+
# cloud_saturation_max: uint8
|
|
529
|
+
result += serializer.pack_value(self.cloud_saturation_max, "uint8")
|
|
530
|
+
# Reserved 23 bytes
|
|
531
|
+
result += serializer.pack_reserved(23)
|
|
541
532
|
|
|
542
533
|
return result
|
|
543
534
|
|
|
@@ -547,49 +538,31 @@ class TileEffectParameter:
|
|
|
547
538
|
from lifx_emulator.protocol import serializer
|
|
548
539
|
|
|
549
540
|
current_offset = offset
|
|
550
|
-
#
|
|
551
|
-
|
|
552
|
-
data, "
|
|
553
|
-
)
|
|
554
|
-
# parameter1: uint32
|
|
555
|
-
parameter1, current_offset = serializer.unpack_value(
|
|
556
|
-
data, "uint32", current_offset
|
|
557
|
-
)
|
|
558
|
-
# parameter2: uint32
|
|
559
|
-
parameter2, current_offset = serializer.unpack_value(
|
|
560
|
-
data, "uint32", current_offset
|
|
561
|
-
)
|
|
562
|
-
# parameter3: uint32
|
|
563
|
-
parameter3, current_offset = serializer.unpack_value(
|
|
564
|
-
data, "uint32", current_offset
|
|
565
|
-
)
|
|
566
|
-
# parameter4: uint32
|
|
567
|
-
parameter4, current_offset = serializer.unpack_value(
|
|
568
|
-
data, "uint32", current_offset
|
|
569
|
-
)
|
|
570
|
-
# parameter5: uint32
|
|
571
|
-
parameter5, current_offset = serializer.unpack_value(
|
|
572
|
-
data, "uint32", current_offset
|
|
541
|
+
# sky_type: TileEffectSkyType (enum)
|
|
542
|
+
sky_type_raw, current_offset = serializer.unpack_value(
|
|
543
|
+
data, "uint8", current_offset
|
|
573
544
|
)
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
545
|
+
sky_type = TileEffectSkyType(sky_type_raw)
|
|
546
|
+
# Skip reserved 3 bytes
|
|
547
|
+
current_offset += 3
|
|
548
|
+
# cloud_saturation_min: uint8
|
|
549
|
+
cloud_saturation_min, current_offset = serializer.unpack_value(
|
|
550
|
+
data, "uint8", current_offset
|
|
577
551
|
)
|
|
578
|
-
#
|
|
579
|
-
|
|
580
|
-
|
|
552
|
+
# Skip reserved 3 bytes
|
|
553
|
+
current_offset += 3
|
|
554
|
+
# cloud_saturation_max: uint8
|
|
555
|
+
cloud_saturation_max, current_offset = serializer.unpack_value(
|
|
556
|
+
data, "uint8", current_offset
|
|
581
557
|
)
|
|
558
|
+
# Skip reserved 23 bytes
|
|
559
|
+
current_offset += 23
|
|
582
560
|
|
|
583
561
|
return (
|
|
584
562
|
cls(
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
parameter3=parameter3,
|
|
589
|
-
parameter4=parameter4,
|
|
590
|
-
parameter5=parameter5,
|
|
591
|
-
parameter6=parameter6,
|
|
592
|
-
parameter7=parameter7,
|
|
563
|
+
sky_type=sky_type,
|
|
564
|
+
cloud_saturation_min=cloud_saturation_min,
|
|
565
|
+
cloud_saturation_max=cloud_saturation_max,
|
|
593
566
|
),
|
|
594
567
|
current_offset,
|
|
595
568
|
)
|
lifx_emulator/server.py
CHANGED
|
@@ -217,7 +217,7 @@ class EmulatedLifxServer:
|
|
|
217
217
|
resp_packet_name = _get_packet_type_name(resp_header.pkt_type)
|
|
218
218
|
resp_fields_str = _format_packet_fields(resp_packet)
|
|
219
219
|
logger.debug(
|
|
220
|
-
"→ TX %s to %s:%s (
|
|
220
|
+
"→ TX %s to %s:%s (target=%s, seq=%s) [%s]",
|
|
221
221
|
resp_packet_name,
|
|
222
222
|
addr[0],
|
|
223
223
|
addr[1],
|
|
@@ -303,7 +303,9 @@ class EmulatedLifxServer:
|
|
|
303
303
|
|
|
304
304
|
# Log received packet with details
|
|
305
305
|
packet_name = _get_packet_type_name(header.pkt_type)
|
|
306
|
-
target_str =
|
|
306
|
+
target_str = (
|
|
307
|
+
"broadcast" if header.tagged else header.target.hex().rstrip("0000")
|
|
308
|
+
)
|
|
307
309
|
fields_str = _format_packet_fields(packet)
|
|
308
310
|
logger.debug(
|
|
309
311
|
"← RX %s from %s:%s (target=%s, seq=%s) [%s]",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
lifx_emulator/__init__.py,sha256=vjhtpAQRSsUZtaUGCQKbmPALvwZ_BF8Mko8w6jzVqBw,819
|
|
2
2
|
lifx_emulator/__main__.py,sha256=zaul9OQhN5csqOqxGWXkVrlurfo2R_-YvM6URk4QAME,21680
|
|
3
3
|
lifx_emulator/constants.py,sha256=DFZkUsdewE-x_3MgO28tMGkjUCWPeYc3xLj_EXViGOw,1032
|
|
4
|
-
lifx_emulator/server.py,sha256=
|
|
4
|
+
lifx_emulator/server.py,sha256=r2JYFcpZIqqhue-Nfq7FbN0KfC3XDf3XDb6b43DsiCk,16438
|
|
5
5
|
lifx_emulator/api/__init__.py,sha256=FoEPw_In5-H_BDQ-XIIONvgj-UqIDVtejIEVRv9qmV8,647
|
|
6
6
|
lifx_emulator/api/app.py,sha256=IxK8sC7MgdtkoLz8iXcEt02nPDaVgdKJgEiGnzTs-YE,4880
|
|
7
7
|
lifx_emulator/api/models.py,sha256=eBx80Ece_4Wv6aqxb1CsZEob9CF0WmR9oJGz3hh14x8,3973
|
|
@@ -21,12 +21,12 @@ lifx_emulator/devices/observers.py,sha256=-KnUgFcKdhlNo7CNVstP-u0wU2W0JAGg055ZPV
|
|
|
21
21
|
lifx_emulator/devices/persistence.py,sha256=9Mhj46-xrweOmyzjORCi2jKIwa8XJWpQ5CgaKcw6U98,10513
|
|
22
22
|
lifx_emulator/devices/state_restorer.py,sha256=eDsRSW-2RviP_0Qlk2DHqMaB-zhV0X1cNQECv2lD1qc,9809
|
|
23
23
|
lifx_emulator/devices/state_serializer.py,sha256=O4Cp3bbGkd4eZf5jzb0MKzWDTgiNhrSGgypmMWaB4dg,5097
|
|
24
|
-
lifx_emulator/devices/states.py,sha256=
|
|
24
|
+
lifx_emulator/devices/states.py,sha256=mVZz7FQeIHLpv2SokmhlQlSBIyVj3GuhGMHBVoFlJqk,10836
|
|
25
25
|
lifx_emulator/factories/__init__.py,sha256=yN8i_Hu_cFEryWZmh0TiOQvWEYFVIApQSs4xeb0EfBk,1170
|
|
26
|
-
lifx_emulator/factories/builder.py,sha256=
|
|
26
|
+
lifx_emulator/factories/builder.py,sha256=OaDqQDGkAyZCSO-4HAsFSd5UzsHpHvRyBk-Fotl1mAY,12056
|
|
27
27
|
lifx_emulator/factories/default_config.py,sha256=FTcxKDfeTmO49GTSki8nxnEIZQzR0Lg0hL_PwHUrkVQ,4828
|
|
28
28
|
lifx_emulator/factories/factory.py,sha256=VQfU5M8zrpFyNHjpGP1q-3bpek9MltBdoAUSvIvt7Bs,7583
|
|
29
|
-
lifx_emulator/factories/firmware_config.py,sha256=
|
|
29
|
+
lifx_emulator/factories/firmware_config.py,sha256=tPN5Hq-uNb1xzW9Q0A9jD-G0-NaGfINcD0i1XZRUMoE,2711
|
|
30
30
|
lifx_emulator/factories/serial_generator.py,sha256=MbaXoommsj76ho8_ZoKuUDnffDf98YvwQiXZSWsUsEs,2507
|
|
31
31
|
lifx_emulator/handlers/__init__.py,sha256=3Hj1hRo3yL3E7GKwG9TaYh33ymk_N3bRiQ8nvqSQULA,1306
|
|
32
32
|
lifx_emulator/handlers/base.py,sha256=0avCLXY_rNlw16PpJ5JrRCwXNE4uMpBqF3PfSfNJ0b8,1654
|
|
@@ -34,19 +34,19 @@ lifx_emulator/handlers/device_handlers.py,sha256=1AmslA4Ut6L7b3SfduDdvnQizTpzUB3
|
|
|
34
34
|
lifx_emulator/handlers/light_handlers.py,sha256=Ryz-_fzoVCT6DBkXhW9YCOYJYaMRcBOIguL3HrQXhAw,11471
|
|
35
35
|
lifx_emulator/handlers/multizone_handlers.py,sha256=2dYsitq0KzEaxEAJmz7ixtir1tvFMOAnfkBQqslqbPM,7914
|
|
36
36
|
lifx_emulator/handlers/registry.py,sha256=s1ht4PmPhXhAcwu1hoY4yW39wy3SPJBMY-9Uxd0FWuE,3292
|
|
37
|
-
lifx_emulator/handlers/tile_handlers.py,sha256=
|
|
37
|
+
lifx_emulator/handlers/tile_handlers.py,sha256=tQSq5hptq_CWlsc0q_I1D3WLcNmOliR47gY2AZI9AEs,12329
|
|
38
38
|
lifx_emulator/products/__init__.py,sha256=qcNop_kRYFF3zSjNemzQEgu3jPrIxfyQyLv9GsnaLEI,627
|
|
39
|
-
lifx_emulator/products/generator.py,sha256=
|
|
39
|
+
lifx_emulator/products/generator.py,sha256=5zcq0iKgwjtg0gePnBEOdIkumesvfzEcKRdBZFPyGtk,33538
|
|
40
40
|
lifx_emulator/products/registry.py,sha256=qkm2xgGZo_ds3wAbYplLu4gb0cxhjZXjnCc1V8etpHw,46517
|
|
41
|
-
lifx_emulator/products/specs.py,sha256=
|
|
42
|
-
lifx_emulator/products/specs.yml,sha256=
|
|
41
|
+
lifx_emulator/products/specs.py,sha256=RxToCvu-iwF6QMcmYgI5CblLOr8hpDZ3k3WT1fZcKe8,8730
|
|
42
|
+
lifx_emulator/products/specs.yml,sha256=eM_r1lTRQHii4dyvnsZWx1P7NzDSZzJl03aR86TY564,9683
|
|
43
43
|
lifx_emulator/protocol/__init__.py,sha256=-wjC-wBcb7fxi5I-mJr2Ad8K2YRflJFdLLdobfD-W1Q,56
|
|
44
|
-
lifx_emulator/protocol/base.py,sha256=
|
|
44
|
+
lifx_emulator/protocol/base.py,sha256=V6t0baSgIXjrsz2dBuUn_V9xwradSqMxBFJHAUtnfCs,15368
|
|
45
45
|
lifx_emulator/protocol/const.py,sha256=ilhv-KcQpHtKh2MDCaIbMLQAsxKO_uTaxyR63v1W8cc,226
|
|
46
46
|
lifx_emulator/protocol/generator.py,sha256=LUkf-1Z5570Vg5iA1QhDZDWQOrABqmukUgk9qH-IJmg,49524
|
|
47
47
|
lifx_emulator/protocol/header.py,sha256=RXMJ5YZG1jyxl4Mz46ZGJBYX41Jdp7J95BHuY-scYC0,5499
|
|
48
48
|
lifx_emulator/protocol/packets.py,sha256=Yv4O-Uqbj0CR7n04vXhfalJVCmTTvJTWkvZBkcwPx-U,41553
|
|
49
|
-
lifx_emulator/protocol/protocol_types.py,sha256=
|
|
49
|
+
lifx_emulator/protocol/protocol_types.py,sha256=WX1p4fmFcNJURmEV_B7ubi7fgu-w9loXQ89q8DdbeSA,23970
|
|
50
50
|
lifx_emulator/protocol/serializer.py,sha256=2bZz7TddxaMRO4_6LujRGCS1w7GxD4E3rRk3r-hpEIE,10738
|
|
51
51
|
lifx_emulator/repositories/__init__.py,sha256=x-ncM6T_Q7jNrwhK4a1uAyMrTGHHGeUzPSLC4O-kEUw,645
|
|
52
52
|
lifx_emulator/repositories/device_repository.py,sha256=KsXVg2sg7PGSTsK_PvDYeHHwEPM9Qx2ZZF_ORncBrYQ,3929
|
|
@@ -55,8 +55,8 @@ lifx_emulator/scenarios/__init__.py,sha256=CGjudoWvyysvFj2xej11N2cr3mYROGtRb9zVH
|
|
|
55
55
|
lifx_emulator/scenarios/manager.py,sha256=1esxRdz74UynNk1wb86MGZ2ZFAuMzByuu74nRe3D-Og,11163
|
|
56
56
|
lifx_emulator/scenarios/models.py,sha256=BKS_fGvrbkGe-vK3arZ0w2f9adS1UZhiOoKpu7GENnc,4099
|
|
57
57
|
lifx_emulator/scenarios/persistence.py,sha256=3vjtPNFYfag38tUxuqxkGpWhQ7uBitc1rLroSAuw9N8,8881
|
|
58
|
-
lifx_emulator-2.
|
|
59
|
-
lifx_emulator-2.
|
|
60
|
-
lifx_emulator-2.
|
|
61
|
-
lifx_emulator-2.
|
|
62
|
-
lifx_emulator-2.
|
|
58
|
+
lifx_emulator-2.2.0.dist-info/METADATA,sha256=IYeBVFaQco74YbRC2s-orGbU4zWnrTrNAnwiT08nxPM,4549
|
|
59
|
+
lifx_emulator-2.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
60
|
+
lifx_emulator-2.2.0.dist-info/entry_points.txt,sha256=R9C_K_tTgt6yXEmhzH4r2Yx2Tu1rLlnYzeG4RFUVzSc,62
|
|
61
|
+
lifx_emulator-2.2.0.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
|
|
62
|
+
lifx_emulator-2.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|