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.
@@ -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
- extended_multizone=self._extended_multizone, override=self._firmware_version
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
- parameter0=0,
243
- parameter1=0,
244
- parameter2=0,
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('"', '\\"')
@@ -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)
@@ -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 Tube Intl
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: 5
230
- tile_height: 11
231
- notes: LIFX Tube Intl
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
@@ -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
- parameter0: int
511
- parameter1: int
512
- parameter2: int
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
- # parameter0: uint32
526
- result += serializer.pack_value(self.parameter0, "uint32")
527
- # parameter1: uint32
528
- result += serializer.pack_value(self.parameter1, "uint32")
529
- # parameter2: uint32
530
- result += serializer.pack_value(self.parameter2, "uint32")
531
- # parameter3: uint32
532
- result += serializer.pack_value(self.parameter3, "uint32")
533
- # parameter4: uint32
534
- result += serializer.pack_value(self.parameter4, "uint32")
535
- # parameter5: uint32
536
- result += serializer.pack_value(self.parameter5, "uint32")
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
- # parameter0: uint32
551
- parameter0, current_offset = serializer.unpack_value(
552
- data, "uint32", current_offset
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
- # parameter6: uint32
575
- parameter6, current_offset = serializer.unpack_value(
576
- data, "uint32", current_offset
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
- # parameter7: uint32
579
- parameter7, current_offset = serializer.unpack_value(
580
- data, "uint32", current_offset
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
- parameter0=parameter0,
586
- parameter1=parameter1,
587
- parameter2=parameter2,
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 (device=%s, seq=%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 = "broadcast" if header.tagged else header.target.hex()
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-emulator
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: LIFX Emulator for testing LIFX LAN protocol libraries
5
5
  Author-email: Avi Miller <me@dje.li>
6
6
  Maintainer-email: Avi Miller <me@dje.li>
@@ -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=0bn7oDIlC6TTOJj9ULXLp9rCFAFcW4vM4whor7VTuRU,16391
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=ealrShXAqEeKYnyNclTGgWxV9uDf3VYyw4SbRHe1xEk,10205
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=ZSz5apcorsKpuPsdjFE4VLC1p41jVY8MWs1-nRBOLMk,11996
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=AzvPvR4pfwjK1yNsaua1L9V1gLVItUVySjcGrXIWnEw,1932
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=D23dQVwukfKccryNEFrojMFhubcg4p-onMCXEDRyTlc,10039
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=NYInVSGyYIxAYMpTihqBtXP06lAYVfbSYe0Wv5Hg9vQ,31758
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=pfmQMrQxlCGqORs3MbsH_vmCvxdaDwjVzXUCVZCjFCI,7093
42
- lifx_emulator/products/specs.yml,sha256=uxzdKFREAHphk8XSPiCHvQE2vwoPfT2m1xy-zC4ZIl4,8552
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=8DyJBhJi9k5LH4qRe-9P-XBC0iUEH01lGodoADH6Za8,13209
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=2Mccm9717EuTXQYaW44W_yReI4EtnlPp3-WEVASgdGY,24820
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.0.0.dist-info/METADATA,sha256=u29qYpMQ0IbZju5mormUZu6Nye04gpQnxBMWnmSYNiM,4549
59
- lifx_emulator-2.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
60
- lifx_emulator-2.0.0.dist-info/entry_points.txt,sha256=R9C_K_tTgt6yXEmhzH4r2Yx2Tu1rLlnYzeG4RFUVzSc,62
61
- lifx_emulator-2.0.0.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
62
- lifx_emulator-2.0.0.dist-info/RECORD,,
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,,