lifx-emulator 2.1.0__py3-none-any.whl → 2.2.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.
@@ -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:
@@ -240,12 +240,25 @@ class GetEffectHandler(PacketHandler):
240
240
  # Create effect settings with Sky parameters
241
241
  from lifx_emulator.protocol.protocol_types import TileEffectSkyType
242
242
 
243
- # Use defaults for SKY effect (type=5), otherwise use stored values
244
- effect_type = device_state.tile_effect_type
245
- if effect_type == 5: # SKY effect
246
- sky_type = device_state.tile_effect_sky_type or 2 # Default to 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
243
+ # Use defaults for SKY effect when values are None, otherwise use stored values
244
+ # NOTE: Must check for None explicitly, not use 'or', because SUNRISE=0 is falsy
245
+ effect_type = TileEffectType(device_state.tile_effect_type)
246
+ if effect_type == TileEffectType.SKY:
247
+ sky_type = (
248
+ device_state.tile_effect_sky_type
249
+ if device_state.tile_effect_sky_type is not None
250
+ else TileEffectSkyType.CLOUDS
251
+ )
252
+ cloud_sat_min = (
253
+ device_state.tile_effect_cloud_sat_min
254
+ if device_state.tile_effect_cloud_sat_min is not None
255
+ else 50
256
+ )
257
+ cloud_sat_max = (
258
+ device_state.tile_effect_cloud_sat_max
259
+ if device_state.tile_effect_cloud_sat_max is not None
260
+ else 180
261
+ )
249
262
  else:
250
263
  sky_type = device_state.tile_effect_sky_type
251
264
  cloud_sat_min = device_state.tile_effect_cloud_sat_min
@@ -284,6 +297,27 @@ class SetEffectHandler(PacketHandler):
284
297
  return []
285
298
 
286
299
  if packet:
300
+ # Sky effect is only supported on LIFX Ceiling devices (176, 177, 201, 202)
301
+ # running firmware 4.4 or higher
302
+ if packet.settings.type == TileEffectType.SKY:
303
+ ceiling_product_ids = {176, 177, 201, 202}
304
+ is_ceiling = device_state.product in ceiling_product_ids
305
+
306
+ # Check firmware version >= 4.4
307
+ firmware_supported = device_state.version_major > 4 or (
308
+ device_state.version_major == 4 and device_state.version_minor >= 4
309
+ )
310
+
311
+ if not (is_ceiling and firmware_supported):
312
+ logger.debug(
313
+ f"Ignoring SKY effect request: "
314
+ f"product={device_state.product}, "
315
+ f"firmware={device_state.version_major}."
316
+ f"{device_state.version_minor} "
317
+ f"(requires Ceiling product and firmware >= 4.4)"
318
+ )
319
+ return []
320
+
287
321
  device_state.tile_effect_type = int(packet.settings.type)
288
322
  device_state.tile_effect_speed = (
289
323
  packet.settings.speed // 1000
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-emulator
3
- Version: 2.1.0
3
+ Version: 2.2.1
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>
@@ -23,10 +23,10 @@ lifx_emulator/devices/state_restorer.py,sha256=eDsRSW-2RviP_0Qlk2DHqMaB-zhV0X1cN
23
23
  lifx_emulator/devices/state_serializer.py,sha256=O4Cp3bbGkd4eZf5jzb0MKzWDTgiNhrSGgypmMWaB4dg,5097
24
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,12 +34,12 @@ 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=-DU4PufPgE7vfvKsZfxP_7vBtI3EtAeBF3-U2-1zyaQ,11294
37
+ lifx_emulator/handlers/tile_handlers.py,sha256=Ci_SrOldf2I8djEd2JJZZ5iR5b4_HvzlWTkaBiCTx7c,12785
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
44
  lifx_emulator/protocol/base.py,sha256=V6t0baSgIXjrsz2dBuUn_V9xwradSqMxBFJHAUtnfCs,15368
45
45
  lifx_emulator/protocol/const.py,sha256=ilhv-KcQpHtKh2MDCaIbMLQAsxKO_uTaxyR63v1W8cc,226
@@ -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.1.0.dist-info/METADATA,sha256=cen3ovCv4G8WJyDQpy46bpFndEZ1OZH09NFuaoE0-mw,4549
59
- lifx_emulator-2.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
60
- lifx_emulator-2.1.0.dist-info/entry_points.txt,sha256=R9C_K_tTgt6yXEmhzH4r2Yx2Tu1rLlnYzeG4RFUVzSc,62
61
- lifx_emulator-2.1.0.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
62
- lifx_emulator-2.1.0.dist-info/RECORD,,
58
+ lifx_emulator-2.2.1.dist-info/METADATA,sha256=AWoxTE55eL8Ga1ORQaXG0gammepJigzKqTrxDj-1UqA,4549
59
+ lifx_emulator-2.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
60
+ lifx_emulator-2.2.1.dist-info/entry_points.txt,sha256=R9C_K_tTgt6yXEmhzH4r2Yx2Tu1rLlnYzeG4RFUVzSc,62
61
+ lifx_emulator-2.2.1.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
62
+ lifx_emulator-2.2.1.dist-info/RECORD,,