lifx-emulator 1.0.1__tar.gz → 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/CLAUDE.md +42 -0
  2. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/PKG-INFO +1 -1
  3. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/changelog.md +8 -0
  4. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/pyproject.toml +9 -1
  5. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/__main__.py +4 -2
  6. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/products/generator.py +30 -18
  7. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/products/registry.py +63 -25
  8. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/uv.lock +1 -1
  9. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/.github/workflows/ci.yml +0 -0
  10. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/.github/workflows/docs.yml +0 -0
  11. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/.gitignore +0 -0
  12. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/.pre-commit-config.yaml +0 -0
  13. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/LICENSE +0 -0
  14. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/README.md +0 -0
  15. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/advanced/device-management-api.md +0 -0
  16. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/advanced/scenario-api.md +0 -0
  17. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/advanced/scenarios.md +0 -0
  18. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/advanced/storage.md +0 -0
  19. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/api/device.md +0 -0
  20. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/api/factories.md +0 -0
  21. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/api/index.md +0 -0
  22. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/api/products.md +0 -0
  23. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/api/protocol.md +0 -0
  24. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/api/server.md +0 -0
  25. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/api/storage.md +0 -0
  26. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/architecture/device-state.md +0 -0
  27. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/architecture/overview.md +0 -0
  28. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/architecture/packet-flow.md +0 -0
  29. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/architecture/protocol.md +0 -0
  30. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/assets/favicon.png +0 -0
  31. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/faq.md +0 -0
  32. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/getting-started/cli.md +0 -0
  33. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/getting-started/installation.md +0 -0
  34. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/getting-started/quickstart.md +0 -0
  35. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/guide/best-practices.md +0 -0
  36. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/guide/device-types.md +0 -0
  37. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/guide/integration-testing.md +0 -0
  38. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/guide/overview.md +0 -0
  39. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/guide/products-and-specs.md +0 -0
  40. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/guide/testing-scenarios.md +0 -0
  41. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/guide/web-interface.md +0 -0
  42. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/index.md +0 -0
  43. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/reference/glossary.md +0 -0
  44. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/reference/troubleshooting.md +0 -0
  45. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/stylesheets/extra.css +0 -0
  46. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/tutorials/01-first-device.md +0 -0
  47. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/tutorials/02-basic.md +0 -0
  48. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/tutorials/03-advanced.md +0 -0
  49. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/tutorials/04-integration.md +0 -0
  50. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/tutorials/05-cicd.md +0 -0
  51. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/docs/tutorials/index.md +0 -0
  52. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/mkdocs.yml +0 -0
  53. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/renovate.json +0 -0
  54. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/__init__.py +0 -0
  55. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/api.py +0 -0
  56. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/async_storage.py +0 -0
  57. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/constants.py +0 -0
  58. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/device.py +0 -0
  59. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/device_states.py +0 -0
  60. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/factories.py +0 -0
  61. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/handlers/__init__.py +0 -0
  62. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/handlers/base.py +0 -0
  63. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/handlers/device_handlers.py +0 -0
  64. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/handlers/light_handlers.py +0 -0
  65. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/handlers/multizone_handlers.py +0 -0
  66. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/handlers/registry.py +0 -0
  67. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/handlers/tile_handlers.py +0 -0
  68. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/observers.py +0 -0
  69. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/products/__init__.py +0 -0
  70. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/products/specs.py +0 -0
  71. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/products/specs.yml +0 -0
  72. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/protocol/__init__.py +0 -0
  73. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/protocol/base.py +0 -0
  74. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/protocol/const.py +0 -0
  75. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/protocol/generator.py +0 -0
  76. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/protocol/header.py +0 -0
  77. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/protocol/packets.py +0 -0
  78. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/protocol/protocol_types.py +0 -0
  79. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/protocol/serializer.py +0 -0
  80. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/scenario_manager.py +0 -0
  81. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/scenario_persistence.py +0 -0
  82. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/server.py +0 -0
  83. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/state_restorer.py +0 -0
  84. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/state_serializer.py +0 -0
  85. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/src/lifx_emulator/storage_protocol.py +0 -0
  86. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/conftest.py +0 -0
  87. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_api.py +0 -0
  88. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_async_storage.py +0 -0
  89. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_cli.py +0 -0
  90. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_cli_validation.py +0 -0
  91. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_device.py +0 -0
  92. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_device_edge_cases.py +0 -0
  93. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_device_handlers_extended.py +0 -0
  94. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_handler_registry.py +0 -0
  95. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_integration.py +0 -0
  96. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_light_handlers_extended.py +0 -0
  97. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_multizone_handlers_extended.py +0 -0
  98. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_observers.py +0 -0
  99. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_products_generator.py +0 -0
  100. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_protocol_generator.py +0 -0
  101. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_protocol_types_coverage.py +0 -0
  102. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_scenario_manager.py +0 -0
  103. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_scenario_persistence.py +0 -0
  104. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_serializer.py +0 -0
  105. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_server.py +0 -0
  106. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_state_restorer.py +0 -0
  107. {lifx_emulator-1.0.1 → lifx_emulator-1.0.2}/tests/test_tile_handlers_extended.py +0 -0
@@ -553,6 +553,48 @@ All factory functions now use the specs system to load product-specific defaults
553
553
  - Tile dimensions (e.g., 8x8 for Tiles, 5x6 for Candles)
554
554
  - Users can override these defaults by passing explicit parameters
555
555
 
556
+ ### Product Registry
557
+
558
+ **Product Registry** (`src/lifx_emulator/products/registry.py`):
559
+ - Auto-generated from official LIFX products.json (https://github.com/LIFX/products)
560
+ - Contains 137+ product definitions with capabilities, temperature ranges, and firmware requirements
561
+ - Pre-built `ProductInfo` instances for efficient runtime lookups
562
+ - Capability flags: `COLOR`, `INFRARED`, `MULTIZONE`, `CHAIN`, `MATRIX`, `RELAYS`, `BUTTONS`, `HEV`, `EXTENDED_MULTIZONE`
563
+ - Never edit this file manually - regenerate using the generator
564
+
565
+ **Product Registry Generator** (`src/lifx_emulator/products/generator.py`):
566
+ - Downloads latest products.json from LIFX GitHub repository
567
+ - Generates optimized Python code with pre-built product definitions
568
+ - Handles extended multizone capability detection:
569
+ - **Native support**: Products with `extended_multizone: true` in features (no firmware requirement)
570
+ - Examples: LIFX Z US (PID 117), LIFX Beam US (PID 119), LIFX Neon, LIFX Permanent Outdoor
571
+ - **Firmware upgrade**: Products with `extended_multizone` in upgrades section (requires minimum firmware)
572
+ - Examples: LIFX Z (PID 32, requires firmware 2.77+), LIFX Beam (PID 38, requires firmware 2.77+)
573
+ - Updates specs.yml with templates for new multizone/matrix products
574
+ - Run with: `python -m lifx_emulator.products.generator`
575
+
576
+ **Product Specs** (`src/lifx_emulator/products/specs.yml`):
577
+ - Product-specific configuration not available in upstream products.json
578
+ - Default zone counts, tile configurations, and device-specific defaults
579
+ - Used by factory functions to create realistic device configurations
580
+ - Manually maintained for accurate product specifications
581
+
582
+ **ProductInfo API:**
583
+ ```python
584
+ from lifx_emulator.products.registry import get_product
585
+
586
+ product = get_product(117) # LIFX Z US
587
+ product.has_extended_multizone # True
588
+ product.min_ext_mz_firmware # None (native support)
589
+ product.supports_extended_multizone() # True
590
+
591
+ product = get_product(32) # LIFX Z (older model)
592
+ product.has_extended_multizone # True
593
+ product.min_ext_mz_firmware # 131149 (firmware 2.77)
594
+ product.supports_extended_multizone(131149) # True (meets requirement)
595
+ product.supports_extended_multizone(131148) # False (below requirement)
596
+ ```
597
+
556
598
  ## Key Implementation Details
557
599
 
558
600
  ### MultiZone Handling
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-emulator
3
- Version: 1.0.1
3
+ Version: 1.0.2
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>
@@ -2,6 +2,14 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.0.2 (2025-11-10)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Extended_multizone added to products correctly by generator
10
+ ([`b6c4f78`](https://github.com/Djelibeybi/lifx-emulator/commit/b6c4f78c7353313b961acdb4283023a595141151))
11
+
12
+
5
13
  ## v1.0.1 (2025-11-10)
6
14
 
7
15
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lifx-emulator"
3
- version = "1.0.1"
3
+ version = "1.0.2"
4
4
  description = "LIFX Emulator for testing LIFX LAN protocol libraries"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -103,6 +103,14 @@ addopts = """\
103
103
  asyncio_mode = "auto"
104
104
  asyncio_default_fixture_loop_scope = "function"
105
105
 
106
+ [tool.coverage.run]
107
+ omit = [
108
+ "src/lifx_emulator/protocol/generator.py",
109
+ "src/lifx_emulator/protocol/protocol_types.py",
110
+ "src/lifx_emulator/products/generator.py",
111
+ "src/lifx_emulator/products/registry.py",
112
+ ]
113
+
106
114
  [tool.coverage.report]
107
115
  exclude_lines = [
108
116
  "pragma: no cover",
@@ -102,10 +102,12 @@ def _format_product_capabilities(product: ProductInfo) -> str:
102
102
  # Add additional capabilities
103
103
  if product.has_infrared:
104
104
  caps.append("infrared")
105
- if product.has_multizone:
106
- caps.append("multizone")
105
+ # Extended multizone is backwards compatible with multizone,
106
+ # so only show multizone if extended multizone is not present
107
107
  if product.has_extended_multizone:
108
108
  caps.append("extended-multizone")
109
+ elif product.has_multizone:
110
+ caps.append("multizone")
109
111
  if product.has_matrix:
110
112
  caps.append("matrix")
111
113
  if product.has_hev:
@@ -97,16 +97,22 @@ def generate_product_definitions(
97
97
  if features.get("hev"):
98
98
  capabilities.append("ProductCapability.HEV")
99
99
 
100
- # Check for extended multizone in upgrades
100
+ # Check for extended multizone capability
101
101
  min_ext_mz_firmware = None
102
- for upgrade in product.get("upgrades", []):
103
- if upgrade.get("features", {}).get("extended_multizone"):
104
- capabilities.append("ProductCapability.EXTENDED_MULTIZONE")
105
- # Parse firmware version (major.minor format)
106
- major = upgrade.get("major", 0)
107
- minor = upgrade.get("minor", 0)
108
- min_ext_mz_firmware = (major << 16) | minor
109
- break
102
+
103
+ # First check if it's a native feature (no firmware requirement)
104
+ if features.get("extended_multizone"):
105
+ capabilities.append("ProductCapability.EXTENDED_MULTIZONE")
106
+ else:
107
+ # Check if it's available as an upgrade (requires minimum firmware)
108
+ for upgrade in product.get("upgrades", []):
109
+ if upgrade.get("features", {}).get("extended_multizone"):
110
+ capabilities.append("ProductCapability.EXTENDED_MULTIZONE")
111
+ # Parse firmware version (major.minor format)
112
+ major = upgrade.get("major", 0)
113
+ minor = upgrade.get("minor", 0)
114
+ min_ext_mz_firmware = (major << 16) | minor
115
+ break
110
116
 
111
117
  # Build capabilities expression
112
118
  if capabilities:
@@ -357,16 +363,22 @@ class ProductRegistry:
357
363
  if features.get("hev"):
358
364
  capabilities |= ProductCapability.HEV
359
365
 
360
- # Check for extended multizone in upgrades
366
+ # Check for extended multizone capability
361
367
  min_ext_mz_firmware = None
362
- for upgrade in product.get("upgrades", []):
363
- if upgrade.get("features", {}).get("extended_multizone"):
364
- capabilities |= ProductCapability.EXTENDED_MULTIZONE
365
- # Parse firmware version (major.minor format)
366
- major = upgrade.get("major", 0)
367
- minor = upgrade.get("minor", 0)
368
- min_ext_mz_firmware = (major << 16) | minor
369
- break
368
+
369
+ # First check if it's a native feature (no firmware requirement)
370
+ if features.get("extended_multizone"):
371
+ capabilities |= ProductCapability.EXTENDED_MULTIZONE
372
+ else:
373
+ # Check if it's available as an upgrade (requires minimum firmware)
374
+ for upgrade in product.get("upgrades", []):
375
+ if upgrade.get("features", {}).get("extended_multizone"):
376
+ capabilities |= ProductCapability.EXTENDED_MULTIZONE
377
+ # Parse firmware version (major.minor format)
378
+ major = upgrade.get("major", 0)
379
+ minor = upgrade.get("minor", 0)
380
+ min_ext_mz_firmware = (major << 16) | minor
381
+ break
370
382
 
371
383
  # Parse temperature range
372
384
  temp_range = None
@@ -677,7 +677,9 @@ PRODUCTS: dict[int, ProductInfo] = {
677
677
  pid=117,
678
678
  name="LIFX Z US",
679
679
  vendor=1,
680
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
680
+ capabilities=ProductCapability.COLOR
681
+ | ProductCapability.MULTIZONE
682
+ | ProductCapability.EXTENDED_MULTIZONE,
681
683
  temperature_range=TemperatureRange(min=1500, max=9000),
682
684
  min_ext_mz_firmware=None,
683
685
  ),
@@ -685,7 +687,9 @@ PRODUCTS: dict[int, ProductInfo] = {
685
687
  pid=118,
686
688
  name="LIFX Z Intl",
687
689
  vendor=1,
688
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
690
+ capabilities=ProductCapability.COLOR
691
+ | ProductCapability.MULTIZONE
692
+ | ProductCapability.EXTENDED_MULTIZONE,
689
693
  temperature_range=TemperatureRange(min=1500, max=9000),
690
694
  min_ext_mz_firmware=None,
691
695
  ),
@@ -693,7 +697,9 @@ PRODUCTS: dict[int, ProductInfo] = {
693
697
  pid=119,
694
698
  name="LIFX Beam US",
695
699
  vendor=1,
696
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
700
+ capabilities=ProductCapability.COLOR
701
+ | ProductCapability.MULTIZONE
702
+ | ProductCapability.EXTENDED_MULTIZONE,
697
703
  temperature_range=TemperatureRange(min=1500, max=9000),
698
704
  min_ext_mz_firmware=None,
699
705
  ),
@@ -701,7 +707,9 @@ PRODUCTS: dict[int, ProductInfo] = {
701
707
  pid=120,
702
708
  name="LIFX Beam Intl",
703
709
  vendor=1,
704
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
710
+ capabilities=ProductCapability.COLOR
711
+ | ProductCapability.MULTIZONE
712
+ | ProductCapability.EXTENDED_MULTIZONE,
705
713
  temperature_range=TemperatureRange(min=1500, max=9000),
706
714
  min_ext_mz_firmware=None,
707
715
  ),
@@ -853,7 +861,9 @@ PRODUCTS: dict[int, ProductInfo] = {
853
861
  pid=141,
854
862
  name="LIFX Neon US",
855
863
  vendor=1,
856
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
864
+ capabilities=ProductCapability.COLOR
865
+ | ProductCapability.MULTIZONE
866
+ | ProductCapability.EXTENDED_MULTIZONE,
857
867
  temperature_range=TemperatureRange(min=1500, max=9000),
858
868
  min_ext_mz_firmware=None,
859
869
  ),
@@ -861,7 +871,9 @@ PRODUCTS: dict[int, ProductInfo] = {
861
871
  pid=142,
862
872
  name="LIFX Neon Intl",
863
873
  vendor=1,
864
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
874
+ capabilities=ProductCapability.COLOR
875
+ | ProductCapability.MULTIZONE
876
+ | ProductCapability.EXTENDED_MULTIZONE,
865
877
  temperature_range=TemperatureRange(min=1500, max=9000),
866
878
  min_ext_mz_firmware=None,
867
879
  ),
@@ -869,7 +881,9 @@ PRODUCTS: dict[int, ProductInfo] = {
869
881
  pid=143,
870
882
  name="LIFX String US",
871
883
  vendor=1,
872
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
884
+ capabilities=ProductCapability.COLOR
885
+ | ProductCapability.MULTIZONE
886
+ | ProductCapability.EXTENDED_MULTIZONE,
873
887
  temperature_range=TemperatureRange(min=1500, max=9000),
874
888
  min_ext_mz_firmware=None,
875
889
  ),
@@ -877,7 +891,9 @@ PRODUCTS: dict[int, ProductInfo] = {
877
891
  pid=144,
878
892
  name="LIFX String Intl",
879
893
  vendor=1,
880
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
894
+ capabilities=ProductCapability.COLOR
895
+ | ProductCapability.MULTIZONE
896
+ | ProductCapability.EXTENDED_MULTIZONE,
881
897
  temperature_range=TemperatureRange(min=1500, max=9000),
882
898
  min_ext_mz_firmware=None,
883
899
  ),
@@ -885,7 +901,9 @@ PRODUCTS: dict[int, ProductInfo] = {
885
901
  pid=161,
886
902
  name="LIFX Outdoor Neon US",
887
903
  vendor=1,
888
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
904
+ capabilities=ProductCapability.COLOR
905
+ | ProductCapability.MULTIZONE
906
+ | ProductCapability.EXTENDED_MULTIZONE,
889
907
  temperature_range=TemperatureRange(min=1500, max=9000),
890
908
  min_ext_mz_firmware=None,
891
909
  ),
@@ -893,7 +911,9 @@ PRODUCTS: dict[int, ProductInfo] = {
893
911
  pid=162,
894
912
  name="LIFX Outdoor Neon Intl",
895
913
  vendor=1,
896
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
914
+ capabilities=ProductCapability.COLOR
915
+ | ProductCapability.MULTIZONE
916
+ | ProductCapability.EXTENDED_MULTIZONE,
897
917
  temperature_range=TemperatureRange(min=1500, max=9000),
898
918
  min_ext_mz_firmware=None,
899
919
  ),
@@ -1101,7 +1121,9 @@ PRODUCTS: dict[int, ProductInfo] = {
1101
1121
  pid=203,
1102
1122
  name="LIFX String US",
1103
1123
  vendor=1,
1104
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
1124
+ capabilities=ProductCapability.COLOR
1125
+ | ProductCapability.MULTIZONE
1126
+ | ProductCapability.EXTENDED_MULTIZONE,
1105
1127
  temperature_range=TemperatureRange(min=1500, max=9000),
1106
1128
  min_ext_mz_firmware=None,
1107
1129
  ),
@@ -1109,7 +1131,9 @@ PRODUCTS: dict[int, ProductInfo] = {
1109
1131
  pid=204,
1110
1132
  name="LIFX String Intl",
1111
1133
  vendor=1,
1112
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
1134
+ capabilities=ProductCapability.COLOR
1135
+ | ProductCapability.MULTIZONE
1136
+ | ProductCapability.EXTENDED_MULTIZONE,
1113
1137
  temperature_range=TemperatureRange(min=1500, max=9000),
1114
1138
  min_ext_mz_firmware=None,
1115
1139
  ),
@@ -1117,7 +1141,9 @@ PRODUCTS: dict[int, ProductInfo] = {
1117
1141
  pid=205,
1118
1142
  name="LIFX Indoor Neon US",
1119
1143
  vendor=1,
1120
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
1144
+ capabilities=ProductCapability.COLOR
1145
+ | ProductCapability.MULTIZONE
1146
+ | ProductCapability.EXTENDED_MULTIZONE,
1121
1147
  temperature_range=TemperatureRange(min=1500, max=9000),
1122
1148
  min_ext_mz_firmware=None,
1123
1149
  ),
@@ -1125,7 +1151,9 @@ PRODUCTS: dict[int, ProductInfo] = {
1125
1151
  pid=206,
1126
1152
  name="LIFX Indoor Neon Intl",
1127
1153
  vendor=1,
1128
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
1154
+ capabilities=ProductCapability.COLOR
1155
+ | ProductCapability.MULTIZONE
1156
+ | ProductCapability.EXTENDED_MULTIZONE,
1129
1157
  temperature_range=TemperatureRange(min=1500, max=9000),
1130
1158
  min_ext_mz_firmware=None,
1131
1159
  ),
@@ -1133,7 +1161,9 @@ PRODUCTS: dict[int, ProductInfo] = {
1133
1161
  pid=213,
1134
1162
  name="LIFX Permanent Outdoor US",
1135
1163
  vendor=1,
1136
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
1164
+ capabilities=ProductCapability.COLOR
1165
+ | ProductCapability.MULTIZONE
1166
+ | ProductCapability.EXTENDED_MULTIZONE,
1137
1167
  temperature_range=TemperatureRange(min=1500, max=9000),
1138
1168
  min_ext_mz_firmware=None,
1139
1169
  ),
@@ -1141,7 +1171,9 @@ PRODUCTS: dict[int, ProductInfo] = {
1141
1171
  pid=214,
1142
1172
  name="LIFX Permanent Outdoor Intl",
1143
1173
  vendor=1,
1144
- capabilities=ProductCapability.COLOR | ProductCapability.MULTIZONE,
1174
+ capabilities=ProductCapability.COLOR
1175
+ | ProductCapability.MULTIZONE
1176
+ | ProductCapability.EXTENDED_MULTIZONE,
1145
1177
  temperature_range=TemperatureRange(min=1500, max=9000),
1146
1178
  min_ext_mz_firmware=None,
1147
1179
  ),
@@ -1300,16 +1332,22 @@ class ProductRegistry:
1300
1332
  if features.get("hev"):
1301
1333
  capabilities |= ProductCapability.HEV
1302
1334
 
1303
- # Check for extended multizone in upgrades
1335
+ # Check for extended multizone capability
1304
1336
  min_ext_mz_firmware = None
1305
- for upgrade in product.get("upgrades", []):
1306
- if upgrade.get("features", {}).get("extended_multizone"):
1307
- capabilities |= ProductCapability.EXTENDED_MULTIZONE
1308
- # Parse firmware version (major.minor format)
1309
- major = upgrade.get("major", 0)
1310
- minor = upgrade.get("minor", 0)
1311
- min_ext_mz_firmware = (major << 16) | minor
1312
- break
1337
+
1338
+ # First check if it's a native feature (no firmware requirement)
1339
+ if features.get("extended_multizone"):
1340
+ capabilities |= ProductCapability.EXTENDED_MULTIZONE
1341
+ else:
1342
+ # Check if it's available as an upgrade (requires minimum firmware)
1343
+ for upgrade in product.get("upgrades", []):
1344
+ if upgrade.get("features", {}).get("extended_multizone"):
1345
+ capabilities |= ProductCapability.EXTENDED_MULTIZONE
1346
+ # Parse firmware version (major.minor format)
1347
+ major = upgrade.get("major", 0)
1348
+ minor = upgrade.get("minor", 0)
1349
+ min_ext_mz_firmware = (major << 16) | minor
1350
+ break
1313
1351
 
1314
1352
  # Parse temperature range
1315
1353
  temp_range = None
@@ -441,7 +441,7 @@ wheels = [
441
441
 
442
442
  [[package]]
443
443
  name = "lifx-emulator"
444
- version = "1.0.1"
444
+ version = "1.0.2"
445
445
  source = { editable = "." }
446
446
  dependencies = [
447
447
  { name = "cyclopts" },
File without changes
File without changes
File without changes
File without changes
File without changes