lifx-emulator 2.3.0__tar.gz → 2.4.0__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 (139) hide show
  1. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/CLAUDE.md +24 -7
  2. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/PKG-INFO +1 -1
  3. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/advanced/device-management-api.md +2 -2
  4. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/advanced/scenario-api.md +2 -2
  5. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/api/device.md +2 -2
  6. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/api/products.md +2 -2
  7. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/api/protocol.md +2 -2
  8. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/architecture/overview.md +1 -1
  9. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/changelog.md +16 -0
  10. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/getting-started/cli.md +16 -6
  11. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/guide/device-types.md +77 -1
  12. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/guide/products-and-specs.md +10 -10
  13. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/guide/web-interface.md +3 -3
  14. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/index.md +1 -1
  15. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/tutorials/04-advanced-scenarios.md +1 -1
  16. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/pyproject.toml +3 -8
  17. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/__main__.py +13 -4
  18. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/api/models.py +2 -2
  19. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/api/templates/dashboard.html +8 -8
  20. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/device.py +58 -2
  21. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/states.py +6 -2
  22. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/factories/__init__.py +2 -0
  23. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/factories/builder.py +4 -2
  24. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/factories/factory.py +35 -4
  25. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/light_handlers.py +155 -20
  26. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/tile_handlers.py +22 -24
  27. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/products/generator.py +77 -35
  28. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/products/registry.py +46 -12
  29. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/products/specs.py +2 -2
  30. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/products/specs.yml +46 -12
  31. lifx_emulator-2.4.0/tests/test_backwards_compatibility.py +943 -0
  32. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_cli.py +4 -4
  33. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_device.py +1 -1
  34. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_integration.py +2 -2
  35. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_products_generator.py +9 -10
  36. lifx_emulator-2.4.0/tests/test_switch_devices.py +335 -0
  37. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_tile_handlers_extended.py +2 -2
  38. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/uv.lock +1 -1
  39. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/.github/workflows/ci.yml +0 -0
  40. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/.github/workflows/docs.yml +0 -0
  41. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/.gitignore +0 -0
  42. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/.pre-commit-config.yaml +0 -0
  43. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/LICENSE +0 -0
  44. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/README.md +0 -0
  45. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/advanced/index.md +0 -0
  46. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/advanced/scenarios.md +0 -0
  47. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/advanced/storage.md +0 -0
  48. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/api/factories.md +0 -0
  49. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/api/index.md +0 -0
  50. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/api/server.md +0 -0
  51. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/api/storage.md +0 -0
  52. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/architecture/device-state.md +0 -0
  53. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/architecture/index.md +0 -0
  54. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/architecture/packet-flow.md +0 -0
  55. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/architecture/protocol.md +0 -0
  56. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/assets/favicon.png +0 -0
  57. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/faq.md +0 -0
  58. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/getting-started/index.md +0 -0
  59. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/getting-started/installation.md +0 -0
  60. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/getting-started/quickstart.md +0 -0
  61. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/guide/best-practices.md +0 -0
  62. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/guide/framebuffers.md +0 -0
  63. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/guide/index.md +0 -0
  64. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/guide/integration-testing.md +0 -0
  65. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/guide/testing-scenarios.md +0 -0
  66. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/reference/glossary.md +0 -0
  67. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/reference/troubleshooting.md +0 -0
  68. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/stylesheets/extra.css +0 -0
  69. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/tutorials/01-first-device.md +0 -0
  70. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/tutorials/02-basic.md +0 -0
  71. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/tutorials/03-integration.md +0 -0
  72. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/tutorials/05-cicd.md +0 -0
  73. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/docs/tutorials/index.md +0 -0
  74. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/mkdocs.yml +0 -0
  75. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/renovate.json +0 -0
  76. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/__init__.py +0 -0
  77. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/api/__init__.py +0 -0
  78. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/api/app.py +0 -0
  79. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/api/mappers/__init__.py +0 -0
  80. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/api/mappers/device_mapper.py +0 -0
  81. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/api/routers/__init__.py +0 -0
  82. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/api/routers/devices.py +0 -0
  83. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/api/routers/monitoring.py +0 -0
  84. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/api/routers/scenarios.py +0 -0
  85. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/api/services/__init__.py +0 -0
  86. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/api/services/device_service.py +0 -0
  87. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/constants.py +0 -0
  88. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/__init__.py +0 -0
  89. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/manager.py +0 -0
  90. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/observers.py +0 -0
  91. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/persistence.py +0 -0
  92. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/state_restorer.py +0 -0
  93. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/state_serializer.py +0 -0
  94. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/factories/default_config.py +0 -0
  95. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/factories/firmware_config.py +0 -0
  96. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/factories/serial_generator.py +0 -0
  97. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/__init__.py +0 -0
  98. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/base.py +0 -0
  99. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/device_handlers.py +0 -0
  100. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/multizone_handlers.py +0 -0
  101. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/registry.py +0 -0
  102. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/products/__init__.py +0 -0
  103. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/__init__.py +0 -0
  104. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/base.py +0 -0
  105. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/const.py +0 -0
  106. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/generator.py +0 -0
  107. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/header.py +0 -0
  108. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/packets.py +0 -0
  109. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/protocol_types.py +0 -0
  110. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/serializer.py +0 -0
  111. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/repositories/__init__.py +0 -0
  112. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/repositories/device_repository.py +0 -0
  113. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/repositories/storage_backend.py +0 -0
  114. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/scenarios/__init__.py +0 -0
  115. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/scenarios/manager.py +0 -0
  116. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/scenarios/models.py +0 -0
  117. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/scenarios/persistence.py +0 -0
  118. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/src/lifx_emulator/server.py +0 -0
  119. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/conftest.py +0 -0
  120. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_api.py +0 -0
  121. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_api_validation.py +0 -0
  122. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_async_storage.py +0 -0
  123. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_cli_validation.py +0 -0
  124. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_device_edge_cases.py +0 -0
  125. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_device_handlers_extended.py +0 -0
  126. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_device_manager.py +0 -0
  127. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_handler_registry.py +0 -0
  128. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_light_handlers_extended.py +0 -0
  129. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_multizone_handlers_extended.py +0 -0
  130. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_observers.py +0 -0
  131. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_products_specs.py +0 -0
  132. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_protocol_generator.py +0 -0
  133. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_protocol_types_coverage.py +0 -0
  134. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_repositories.py +0 -0
  135. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_scenario_manager.py +0 -0
  136. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_scenario_persistence.py +0 -0
  137. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_serializer.py +0 -0
  138. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_server.py +0 -0
  139. {lifx_emulator-2.3.0 → lifx_emulator-2.4.0}/tests/test_state_restorer.py +0 -0
@@ -70,7 +70,7 @@ lifx-emulator --bind 192.168.1.100 --port 56700
70
70
  lifx-emulator --color 2 --multizone 1 --tile 1 --verbose
71
71
 
72
72
  # Create only specific device types
73
- lifx-emulator --color 0 --infrared 3 --hev 2
73
+ lifx-emulator --color 0 --infrared 3 --hev 2 --switch 2
74
74
 
75
75
  # Mix product IDs with device types
76
76
  lifx-emulator --product 27 --color 2 --multizone 1
@@ -128,8 +128,9 @@ lifx-emulator --help
128
128
  - `--multizone-extended`: Enable extended multizone support (default: True, use --no-multizone-extended to disable)
129
129
  - `--tile`: Number of tile devices
130
130
  - `--tile-count`: Tiles per device (uses product default if not specified)
131
- - `--tile-width`: Width of each tile in pixels (uses product default if not specified)
132
- - `--tile-height`: Height of each tile in pixels (uses product default if not specified)
131
+ - `--tile-width`: Width of each tile in zones (uses product default if not specified)
132
+ - `--tile-height`: Height of each tile in zones (uses product default if not specified)
133
+ - `--switch`: Number of LIFX Switch devices (relays, no lighting, default: 0)
133
134
  - `--serial-prefix`: serial prefix (6 hex chars, default: d073d5)
134
135
  - `--serial-start`: Starting serial suffix (default: 1)
135
136
  - `--api`: Enable HTTP API server for monitoring and management (default: False)
@@ -140,9 +141,9 @@ lifx-emulator --help
140
141
  **Product Defaults:**
141
142
  Device parameters like `--multizone-zones` and `--tile-count` automatically use product-specific defaults from the specs system when not specified:
142
143
  - LIFX Beam: Extended multizone support enabled and 80 zones by default
143
- - LIFX Tile: 5 tiles of 8x8 pixels by default
144
- - LIFX Candle: 1 tile of 5x6 pixels by default
145
- - LIFX Ceiling: 1 tile of 8x8 pixels by default
144
+ - LIFX Tile: 5 tiles of 8x8 zones by default
145
+ - LIFX Candle: 1 tile of 5x6 zones by default
146
+ - LIFX Ceiling: 1 tile of 8x8 zones by default
146
147
  - These defaults can be overridden with command-line parameters
147
148
 
148
149
  **Firmware Version:**
@@ -521,7 +522,7 @@ delay = manager.get_response_delay(502, merged) # 1.0s
521
522
 
522
523
  **DeviceState** (`src/lifx_emulator/devices/states.py`):
523
524
  - Dataclass holding all device state (color, power, zones, tiles, firmware version, etc.)
524
- - Capability flags: `has_color`, `has_infrared`, `has_multizone`, `has_matrix`, `has_hev`
525
+ - Capability flags: `has_color`, `has_infrared`, `has_multizone`, `has_matrix`, `has_hev`, `has_relays`, `has_buttons`
525
526
  - Initialized differently per device type via factory functions
526
527
  - **TileFramebuffers**: Internal dataclass for storing non-visible framebuffers (1-7) per tile
527
528
  - Provides `get_framebuffer(fb_index, width, height)` for lazy initialization
@@ -562,6 +563,10 @@ delay = manager.get_response_delay(502, merged) # 1.0s
562
563
  - Zone count uses product defaults from specs if not specified
563
564
  - `create_tile_device(tile_count=None)`: Tile chain (product=55)
564
565
  - Tile count and dimensions use product defaults from specs if not specified
566
+ - `create_switch(product_id=70)`: LIFX Switch device (product=70)
567
+ - Has `has_relays=True` and `has_buttons=True` capabilities
568
+ - No lighting control (responds with StateUnhandled to Light/MultiZone/Tile packets)
569
+ - Supports all Device.* packets for basic device information
565
570
  - `create_device(product_id, zone_count=None, tile_count=None)`: Universal factory
566
571
  - Creates any device by product ID from the registry
567
572
  - Automatically uses product defaults from specs system
@@ -764,6 +769,18 @@ Matrix devices support 8 framebuffers (0-7) for advanced rendering:
764
769
  - Framebuffers are lazily initialized on first access
765
770
  - Non-visible framebuffers are persisted with device state
766
771
 
772
+ ### Switch Handling
773
+ - LIFX Switch devices have `has_relays=True` and `has_buttons=True` capabilities
774
+ - Switches do not support lighting operations (no color, brightness, or zone control)
775
+ - **Capability-based packet filtering**: Switches automatically return `StateUnhandled` (packet 223) for:
776
+ - Light.* packets (types 101-149): GetColor, SetColor, SetWaveform, etc.
777
+ - MultiZone.* packets (types 501-512): GetColorZones, SetColorZones, etc.
778
+ - Tile.* packets (types 701-720): Get64, Set64, GetTileEffect, etc.
779
+ - Switches handle Device.* packets (types 2-59) normally: GetVersion, GetLabel, EchoRequest, etc.
780
+ - StateUnhandled response includes the `unhandled_type` field indicating which packet type was rejected
781
+ - Acknowledgments (packet 45) are still sent if `ack_required=True` flag is set
782
+ - **Note**: Button and relay protocol packets are not currently implemented (requires cloud/Matter infrastructure)
783
+
767
784
  ### Testing Scenarios
768
785
  Configure via ScenarioConfig in HierarchicalScenarioManager:
769
786
  - `drop_packets`: Dict mapping packet type to drop rate (0.0-1.0, where 1.0 = always drop)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-emulator
3
- Version: 2.3.0
3
+ Version: 2.4.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>
@@ -266,8 +266,8 @@ Creates a new emulated device by product ID. The device will be added to the emu
266
266
  - `serial` (optional): Device serial (auto-generated if not provided)
267
267
  - `zone_count` (optional): Number of zones for multizone devices
268
268
  - `tile_count` (optional): Number of tiles for matrix devices
269
- - `tile_width` (optional): Width of each tile in pixels
270
- - `tile_height` (optional): Height of each tile in pixels
269
+ - `tile_width` (optional): Width of each tile in zones
270
+ - `tile_height` (optional): Height of each tile in zones
271
271
  - `firmware_major` (optional): Firmware major version
272
272
  - `firmware_minor` (optional): Firmware minor version
273
273
 
@@ -788,8 +788,8 @@ jobs:
788
788
  | 506 | StateMultiZone | Response with zone colors |
789
789
  | 512 | ExtendedStateMultiZone | Response with extended zones |
790
790
  | 701 | GetDeviceChain | Get tile chain info |
791
- | 707 | Get64 | Get tile pixel data |
792
- | 715 | Set64 | Set tile pixel data |
791
+ | 707 | Get64 | Get tile zone data |
792
+ | 715 | Set64 | Set tile zone data |
793
793
 
794
794
  ## Tips and Best Practices
795
795
 
@@ -90,8 +90,8 @@ Dataclass holding all stateful information for an emulated LIFX device.
90
90
 
91
91
  - **`tile_count`** (`int` = `0`) - Number of tiles in chain
92
92
  - **`tile_devices`** (`list[dict]` = `[]`) - Per-tile state (position, colors)
93
- - **`tile_width`** (`int` = `8`) - Width of each tile in pixels
94
- - **`tile_height`** (`int` = `8`) - Height of each tile in pixels
93
+ - **`tile_width`** (`int` = `8`) - Width of each tile in zones
94
+ - **`tile_height`** (`int` = `8`) - Height of each tile in zones
95
95
 
96
96
  #### Effects (Waveforms & Animations)
97
97
 
@@ -239,8 +239,8 @@ Get detailed specifications for a product.
239
239
  - `zone_count`: Number of zones (multizone devices)
240
240
  - `extended_multizone`: Extended multizone support flag
241
241
  - `tile_count`: Default number of tiles (matrix devices)
242
- - `tile_width`: Tile width in pixels (matrix devices)
243
- - `tile_height`: Tile height in pixels (matrix devices)
242
+ - `tile_width`: Tile width in zones (matrix devices)
243
+ - `tile_height`: Tile height in zones (matrix devices)
244
244
 
245
245
  **Example:**
246
246
  ```python
@@ -322,8 +322,8 @@ class TileStateDevice:
322
322
  accel_meas_z: int
323
323
  user_x: float # User-configured X position
324
324
  user_y: float # User-configured Y position
325
- width: int # Tile width in pixels (e.g., 8)
326
- height: int # Tile height in pixels (e.g., 8)
325
+ width: int # Tile width in zones (e.g., 8)
326
+ height: int # Tile height in zones (e.g., 8)
327
327
  device_version_vendor: int
328
328
  device_version_product: int
329
329
  device_version_version: int
@@ -253,7 +253,7 @@ Devices advertise capabilities through boolean flags:
253
253
  | `has_infrared` | IR brightness | LIFX A19 Night Vision |
254
254
  | `has_multizone` | Linear zones | LIFX Z, LIFX Beam |
255
255
  | `has_extended_multizone` | >16 zones | LIFX Beam |
256
- | `has_matrix` | 2D pixel grid | LIFX Tile, LIFX Candle |
256
+ | `has_matrix` | 2D zone grid | LIFX Tile, LIFX Candle |
257
257
  | `has_hev` | HEV cleaning | LIFX Clean |
258
258
 
259
259
  ## Packet Types
@@ -2,6 +2,22 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v2.4.0 (2025-11-19)
6
+
7
+ ### Features
8
+
9
+ - Implement LIFX Switch device emulation with StateUnhandled responses
10
+ ([`e2f9114`](https://github.com/Djelibeybi/lifx-emulator/commit/e2f911420db1d27d916247a9b3fdb50f31276b48))
11
+
12
+
13
+ ## v2.3.1 (2025-11-18)
14
+
15
+ ### Bug Fixes
16
+
17
+ - **tile**: Implement backwards compatibility on tile and multizone devices
18
+ ([`be473f7`](https://github.com/Djelibeybi/lifx-emulator/commit/be473f74f9a49be7f07082d5bfba16f5d74f46f6))
19
+
20
+
5
21
  ## v2.3.0 (2025-11-18)
6
22
 
7
23
  ### Features
@@ -197,20 +197,30 @@ Number of tiles per tile device. If not specified, uses product default (5 for L
197
197
  - **Default:** `None` (uses product defaults)
198
198
  - **Example:** `--tile-count 10`
199
199
 
200
- ### `--tile-width <PIXELS>`
200
+ ### `--tile-width <zones>`
201
201
 
202
- Width of each tile in pixels. If not specified, uses product default (typically 8).
202
+ Width of each tile in zones. If not specified, uses product default (typically 8).
203
203
 
204
204
  - **Default:** `None` (uses product defaults)
205
205
  - **Example:** `--tile-width 16`
206
206
 
207
- ### `--tile-height <PIXELS>`
207
+ ### `--tile-height <zones>`
208
208
 
209
- Height of each tile in pixels. If not specified, uses product default (typically 8).
209
+ Height of each tile in zones. If not specified, uses product default (typically 8).
210
210
 
211
211
  - **Default:** `None` (uses product defaults)
212
212
  - **Example:** `--tile-height 8`
213
213
 
214
+ ### `--switch <COUNT>`
215
+
216
+ Number of LIFX Switch devices to emulate (relay-based switches with no lighting).
217
+
218
+ - **Default:** `0`
219
+ - **Product:** 70 (LIFX Switch)
220
+ - **Example:** `--switch 2`
221
+
222
+ Switch devices have `has_relays=True` and `has_buttons=True` capabilities but do not support Light, MultiZone, or Tile protocol packets. They respond with `StateUnhandled` (packet 223) to unsupported requests.
223
+
214
224
  ## serial Options
215
225
 
216
226
  ### `--serial-prefix <PREFIX>`
@@ -291,8 +301,8 @@ lifx-emulator --serial-prefix cafe00 --serial-start 100 --color 3
291
301
  ### Only Specific Types
292
302
 
293
303
  ```bash
294
- # No default devices, only infrared and HEV
295
- lifx-emulator --color 0 --infrared 3 --HEV 2
304
+ # No default devices, only infrared, HEV, and switches
305
+ lifx-emulator --color 0 --infrared 3 --HEV 2 --switch 2
296
306
  ```
297
307
 
298
308
  ### Discovery Testing
@@ -297,7 +297,7 @@ print(f"Tile height: {device.state.tile_height}") # 8
297
297
 
298
298
  # Access tile devices
299
299
  for i, tile in enumerate(device.state.tile_devices):
300
- print(f"Tile {i}: {tile.width}x{tile.height} pixels")
300
+ print(f"Tile {i}: {tile.width}x{tile.height} zones")
301
301
  ```
302
302
 
303
303
  ### Matrix Packet Types
@@ -341,6 +341,82 @@ For large tiles (>64 zones), prepare all zones in a non-visible framebuffer, the
341
341
  See [Framebuffer Guide](framebuffers.md) for complete documentation and examples.
342
342
 
343
343
 
344
+ ## Switch Devices
345
+
346
+ LIFX Switch devices are relay-based switches with no lighting capabilities. They respond with `StateUnhandled` (packet 223) to all lighting-related protocol requests.
347
+
348
+ ### Example Products
349
+
350
+ - **LIFX Switch** (product IDs 70, 71, 89, 115, 116) - 2 relay switches
351
+
352
+ ### Capabilities
353
+
354
+ - **Relays**: Physical relay switches for controlling external loads
355
+ - **Buttons**: Physical buttons for manual control
356
+ - **No lighting**: No color, brightness, or zone control
357
+ - Basic device operations (GetVersion, GetLabel, EchoRequest, etc.)
358
+
359
+ ### Factory Function
360
+
361
+ ```python
362
+ from lifx_emulator import create_switch
363
+
364
+ # Create LIFX Switch (default product 70)
365
+ switch = create_switch("d073d7000001")
366
+
367
+ # Or specify a different switch product
368
+ switch = create_switch("d073d7000002", product_id=89)
369
+ ```
370
+
371
+ ### Switch Behavior
372
+
373
+ ```python
374
+ switch = create_switch("d073d7000001")
375
+
376
+ # Check capabilities
377
+ print(f"Has relays: {switch.state.has_relays}") # True
378
+ print(f"Has buttons: {switch.state.has_buttons}") # True
379
+ print(f"Has color: {switch.state.has_color}") # False
380
+ print(f"Has multizone: {switch.state.has_multizone}") # False
381
+ ```
382
+
383
+ ### Packet Handling
384
+
385
+ **Supported (Device.* packets 2-59):**
386
+ - `GetVersion` (32) → `StateVersion` (33)
387
+ - `GetLabel` (23) → `StateLabel` (25)
388
+ - `SetLabel` (24)
389
+ - `EchoRequest` (58) → `EchoResponse` (59)
390
+ - All other Device.* packets
391
+
392
+ **Rejected with StateUnhandled (223):**
393
+ - **Light.* packets (101-149)**: GetColor, SetColor, GetPower, SetPower, etc.
394
+ - **MultiZone.* packets (501-512)**: GetColorZones, SetColorZones, etc.
395
+ - **Tile.* packets (701-720)**: Get64, Set64, GetTileEffect, etc.
396
+
397
+ ### StateUnhandled Response
398
+
399
+ When a switch receives an unsupported packet type, it responds with:
400
+
401
+ ```python
402
+ # Client sends Light.GetColor (101) to switch
403
+ # Switch responds with:
404
+ # - StateUnhandled (223) with unhandled_type=101
405
+ # - Acknowledgement (45) if ack_required=True
406
+ ```
407
+
408
+ The `StateUnhandled` packet includes the rejected packet type in the `unhandled_type` field, allowing clients to detect and handle unsupported operations gracefully.
409
+
410
+ ### Limitations
411
+
412
+ **Note**: Button and relay control protocol packets are not currently implemented in the emulator.
413
+
414
+ The switch emulation is primarily for testing client libraries' handling of:
415
+ - Device capability detection
416
+ - StateUnhandled response handling
417
+ - Graceful degradation when lighting features are unavailable
418
+
419
+
344
420
  ## Using Generic create_device()
345
421
 
346
422
  All factory functions use `create_device()` internally. You can use it directly:
@@ -31,7 +31,7 @@ Contains product-specific details not available in the upstream catalog:
31
31
  - Min/max zone counts
32
32
  - Default tile counts for matrix devices
33
33
  - Min/max tile counts
34
- - Tile dimensions (width x height in pixels)
34
+ - Tile dimensions (width x height in zones)
35
35
  - Product-specific notes
36
36
 
37
37
  ### `specs.py`
@@ -81,8 +81,8 @@ products:
81
81
  default_tile_count: <number> # Typical number of tiles in chain
82
82
  min_tile_count: <number> # Minimum tiles supported
83
83
  max_tile_count: <number> # Maximum tiles supported
84
- tile_width: <pixels> # Width of each tile
85
- tile_height: <pixels> # Height of each tile
84
+ tile_width: <zones> # Width of each tile
85
+ tile_height: <zones> # Height of each tile
86
86
  notes: "<description>"
87
87
  ```
88
88
 
@@ -95,7 +95,7 @@ products:
95
95
  max_tile_count: 5
96
96
  tile_width: 8
97
97
  tile_height: 8
98
- notes: "LIFX Tile, 8x8 pixel matrix, chainable up to 5"
98
+ notes: "LIFX Tile, 8x8 zone matrix, chainable up to 5"
99
99
  ```
100
100
 
101
101
  **Example - LIFX Candle:**
@@ -107,7 +107,7 @@ products:
107
107
  max_tile_count: 1
108
108
  tile_width: 5
109
109
  tile_height: 6
110
- notes: "LIFX Candle, 5x6 pixel matrix, single unit"
110
+ notes: "LIFX Candle, 5x6 zone matrix, single unit"
111
111
  ```
112
112
 
113
113
  **Example - LIFX Ceiling:**
@@ -119,7 +119,7 @@ products:
119
119
  max_tile_count: 1
120
120
  tile_width: 22
121
121
  tile_height: 22
122
- notes: "LIFX Ceiling, 22x22 pixel matrix"
122
+ notes: "LIFX Ceiling, 22x22 zone matrix"
123
123
  ```
124
124
 
125
125
  ## How Specifications Are Used
@@ -149,16 +149,16 @@ When creating a matrix device:
149
149
  2. **Tile count**: From `specs.yml` if not specified by user
150
150
 
151
151
  ```python
152
- # Uses specification: 5 tiles of 8x8 pixels
152
+ # Uses specification: 5 tiles of 8x8 zones
153
153
  device = create_device(55)
154
154
 
155
155
  # Custom tile count, specification dimensions
156
- device = create_device(55, tile_count=3) # 3 tiles of 8x8 pixels
156
+ device = create_device(55, tile_count=3) # 3 tiles of 8x8 zones
157
157
 
158
- # Candle: 1 tile of 5x5 pixels (from specification)
158
+ # Candle: 1 tile of 5x5 zones (from specification)
159
159
  device = create_device(57)
160
160
 
161
- # Ceiling: 1 tile of 22x22 pixels (from specification)
161
+ # Ceiling: 1 tile of 22x22 zones (from specification)
162
162
  device = create_device(176)
163
163
  ```
164
164
 
@@ -142,7 +142,7 @@ For matrix/tile devices:
142
142
  ```
143
143
  ▸ Show tiles (5) (click to expand)
144
144
  ┌──────────┐
145
- │ T1 │ (8×8 pixel grid)
145
+ │ T1 │ (8×8 zone grid)
146
146
  │ ████████ │
147
147
  │ ████████ │
148
148
  │ ████████ │
@@ -249,8 +249,8 @@ For matrix devices (tiles, candles, ceiling):
249
249
 
250
250
  1. Locate device in Devices section
251
251
  2. Click "▸ Show tiles" to expand
252
- 3. Grid display shows pixel colors
253
- 4. Each small square is one pixel
252
+ 3. Grid display shows zone colors
253
+ 4. Each small square is one zone
254
254
  5. Tiles labeled T1, T2, etc.
255
255
  6. Click again to collapse
256
256
 
@@ -71,7 +71,7 @@ LIFX Emulator is a Python library and CLI tool that creates virtual LIFX devices
71
71
  | Infrared | LIFX A19 Night Vision | IR brightness control |
72
72
  | HEV | LIFX Clean | HEV cleaning cycle |
73
73
  | Multizone | LIFX Z, LIFX Beam | Linear zones (up to 82) |
74
- | Matrix | LIFX Tile, LIFX Candle | 2D pixel arrays |
74
+ | Matrix | LIFX Tile, LIFX Candle | 2D zone arrays |
75
75
 
76
76
  ## Use Cases
77
77
 
@@ -84,7 +84,7 @@ async def main():
84
84
  # Create a LIFX Tile with 5 tiles in the chain
85
85
  device = create_tile_device("d073d9000001", tile_count=5)
86
86
 
87
- # Each tile is 8x8 pixels (64 zones)
87
+ # Each tile is 8x8 zones (64 zones)
88
88
  print(f"Tile device configuration:")
89
89
  print(f" Tiles: {len(device.state.tile_devices)}")
90
90
  for i, tile in enumerate(device.state.tile_devices):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lifx-emulator"
3
- version = "2.3.0"
3
+ version = "2.4.0"
4
4
  description = "LIFX Emulator for testing LIFX LAN protocol libraries"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -73,7 +73,7 @@ docstring-code-format = true
73
73
  docstring-code-line-length = "dynamic"
74
74
 
75
75
  [tool.ruff.lint]
76
- select = ["E", "F", "I", "N", "W", "UP", "C90"] # C90 = mccabe complexity
76
+ select = ["E", "F", "I", "N", "W", "UP"]
77
77
  ignore = []
78
78
 
79
79
  # Complexity and code quality limits
@@ -86,13 +86,8 @@ max-branches = 12
86
86
  max-statements = 50
87
87
 
88
88
  [tool.ruff.lint.per-file-ignores]
89
- "src/lifx_emulator/__main__.py" = ["C901"]
90
- "src/lifx_emulator/devices/state_restorer.py" = ["C901"]
91
- "src/lifx_emulator/handlers/tile_handlers.py" = ["C901"]
92
- "src/lifx_emulator/protocol/generator.py" = ["E501", "C901"]
89
+ "src/lifx_emulator/protocol/generator.py" = ["E501"]
93
90
  "src/lifx_emulator/protocol/packets.py" = ["E501"]
94
- "src/lifx_emulator/products/registry.py" = ["C901"]
95
- "src/lifx_emulator/scenarios/persistence.py" = ["C901"]
96
91
 
97
92
 
98
93
  [tool.pyright]
@@ -21,6 +21,7 @@ from lifx_emulator.factories import (
21
21
  create_hev_light,
22
22
  create_infrared_light,
23
23
  create_multizone_light,
24
+ create_switch,
24
25
  create_tile_device,
25
26
  )
26
27
  from lifx_emulator.products.registry import get_registry
@@ -239,6 +240,7 @@ async def run(
239
240
  hev: Annotated[int, cyclopts.Parameter(group=device_group)] = 0,
240
241
  multizone: Annotated[int, cyclopts.Parameter(group=device_group)] = 0,
241
242
  tile: Annotated[int, cyclopts.Parameter(group=device_group)] = 0,
243
+ switch: Annotated[int, cyclopts.Parameter(group=device_group)] = 0,
242
244
  # Multizone Options
243
245
  multizone_zones: Annotated[
244
246
  int | None, cyclopts.Parameter(group=multizone_group)
@@ -284,11 +286,12 @@ async def run(
284
286
  multizone_extended: Enable extended multizone support (Beam).
285
287
  Set --no-multizone-extended for basic multizone (Z) devices.
286
288
  tile: Number of tile/matrix chain devices.
289
+ switch: Number of LIFX Switch devices (relays, no lighting).
287
290
  tile_count: Number of tiles per device. Uses product defaults if not
288
291
  specified (5 for Tile, 1 for Candle/Ceiling).
289
- tile_width: Width of each tile in pixels. Uses product defaults if not
292
+ tile_width: Width of each tile in zones. Uses product defaults if not
290
293
  specified (8 for most devices).
291
- tile_height: Height of each tile in pixels. Uses product defaults if
294
+ tile_height: Height of each tile in zones. Uses product defaults if
292
295
  not specified (8 for most devices).
293
296
  serial_prefix: Serial number prefix as 6 hex characters.
294
297
  serial_start: Starting serial suffix for auto-incrementing device serials.
@@ -310,7 +313,7 @@ async def run(
310
313
  lifx-emulator --color 2 --multizone 1 --tile 1 --api --verbose
311
314
 
312
315
  Create only specific device types:
313
- lifx-emulator --color 0 --infrared 3 --hev 2
316
+ lifx-emulator --color 0 --infrared 3 --hev 2 --switch 2
314
317
 
315
318
  Custom serial prefix:
316
319
  lifx-emulator --serial-prefix cafe00 --color 5
@@ -410,6 +413,7 @@ async def run(
410
413
  and infrared == 0
411
414
  and hev == 0
412
415
  and multizone == 0
416
+ and switch == 0
413
417
  ):
414
418
  color = 0
415
419
 
@@ -423,6 +427,7 @@ async def run(
423
427
  and hev == 0
424
428
  and multizone == 0
425
429
  and tile == 0
430
+ and switch == 0
426
431
  ):
427
432
  color = 0
428
433
 
@@ -467,13 +472,17 @@ async def run(
467
472
  )
468
473
  )
469
474
 
475
+ # Create switch devices
476
+ for _ in range(switch):
477
+ devices.append(create_switch(get_serial(), storage=storage))
478
+
470
479
  if not devices:
471
480
  if persistent:
472
481
  logger.warning("No devices configured. Server will run with no devices.")
473
482
  logger.info("Use API (--api) or restart with device flags to add devices.")
474
483
  else:
475
484
  logger.error(
476
- "No devices configured. Use --color, --multizone, --tile, "
485
+ "No devices configured. Use --color, --multizone, --tile, --switch, "
477
486
  "etc. to add devices."
478
487
  )
479
488
  return
@@ -25,10 +25,10 @@ class DeviceCreateRequest(BaseModel):
25
25
  None, description="Number of tiles for matrix devices", ge=0, le=100
26
26
  )
27
27
  tile_width: int | None = Field(
28
- None, description="Width of each tile in pixels", ge=1, le=256
28
+ None, description="Width of each tile in zones", ge=1, le=256
29
29
  )
30
30
  tile_height: int | None = Field(
31
- None, description="Height of each tile in pixels", ge=1, le=256
31
+ None, description="Height of each tile in zones", ge=1, le=256
32
32
  )
33
33
  firmware_major: int | None = Field(
34
34
  None, description="Firmware major version", ge=0, le=255
@@ -163,7 +163,7 @@
163
163
  gap: 2px;
164
164
  margin-top: 4px;
165
165
  }
166
- .tile-pixel {
166
+ .tile-zone {
167
167
  width: 8px;
168
168
  height: 8px;
169
169
  border-radius: 1px;
@@ -644,7 +644,7 @@
644
644
  `;
645
645
  } else if (dev.has_matrix && dev.tile_devices &&
646
646
  dev.tile_devices.length > 0) {
647
- // Render actual tile pixels
647
+ // Render actual tile zones
648
648
  const tilesHtml = dev.tile_devices.map((tile, tileIndex) => {
649
649
  if (!tile.colors || tile.colors.length === 0) {
650
650
  return '<div style="color: #666;">No color data</div>';
@@ -652,14 +652,14 @@
652
652
 
653
653
  const width = tile.width || 8;
654
654
  const height = tile.height || 8;
655
- const totalPixels = width * height;
655
+ const totalzones = width * height;
656
656
 
657
- // Create grid of pixels
658
- const slicedColors = tile.colors.slice(0, totalPixels);
659
- const pixelsHtml = slicedColors.map(color => {
657
+ // Create grid of zones
658
+ const slicedColors = tile.colors.slice(0, totalzones);
659
+ const zonesHtml = slicedColors.map(color => {
660
660
  const rgb = hsbkToRgb(color);
661
661
  const bgStyle = `background: ${rgb};`;
662
- return `<div class="tile-pixel" style="${bgStyle}"></div>`;
662
+ return `<div class="tile-zone" style="${bgStyle}"></div>`;
663
663
  }).join('');
664
664
 
665
665
  const labelStyle = (
@@ -675,7 +675,7 @@
675
675
  T${tileIndex + 1}
676
676
  </div>
677
677
  <div class="tile-grid" style="${gridStyle}">
678
- ${pixelsHtml}
678
+ ${zonesHtml}
679
679
  </div>
680
680
  </div>
681
681
  `;