lifx-emulator 2.3.1__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.
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/CLAUDE.md +19 -2
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/PKG-INFO +1 -1
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/changelog.md +8 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/getting-started/cli.md +12 -2
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/guide/device-types.md +76 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/pyproject.toml +1 -1
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/__main__.py +11 -2
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/device.py +56 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/states.py +4 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/factories/__init__.py +2 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/factories/builder.py +2 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/factories/factory.py +31 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/products/generator.py +75 -33
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/products/registry.py +46 -12
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/products/specs.yml +38 -4
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_cli.py +4 -4
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_products_generator.py +9 -10
- lifx_emulator-2.4.0/tests/test_switch_devices.py +335 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/uv.lock +1 -1
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/.github/workflows/ci.yml +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/.github/workflows/docs.yml +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/.gitignore +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/.pre-commit-config.yaml +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/LICENSE +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/README.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/advanced/device-management-api.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/advanced/index.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/advanced/scenario-api.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/advanced/scenarios.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/advanced/storage.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/api/device.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/api/factories.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/api/index.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/api/products.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/api/protocol.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/api/server.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/api/storage.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/architecture/device-state.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/architecture/index.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/architecture/overview.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/architecture/packet-flow.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/architecture/protocol.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/assets/favicon.png +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/faq.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/getting-started/index.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/getting-started/installation.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/getting-started/quickstart.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/guide/best-practices.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/guide/framebuffers.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/guide/index.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/guide/integration-testing.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/guide/products-and-specs.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/guide/testing-scenarios.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/guide/web-interface.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/index.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/reference/glossary.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/reference/troubleshooting.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/stylesheets/extra.css +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/tutorials/01-first-device.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/tutorials/02-basic.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/tutorials/03-integration.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/tutorials/04-advanced-scenarios.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/tutorials/05-cicd.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/docs/tutorials/index.md +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/mkdocs.yml +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/renovate.json +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/__init__.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/api/__init__.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/api/app.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/api/mappers/__init__.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/api/mappers/device_mapper.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/api/models.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/api/routers/__init__.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/api/routers/devices.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/api/routers/monitoring.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/api/routers/scenarios.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/api/services/__init__.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/api/services/device_service.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/api/templates/dashboard.html +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/constants.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/__init__.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/manager.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/observers.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/persistence.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/state_restorer.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/devices/state_serializer.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/factories/default_config.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/factories/firmware_config.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/factories/serial_generator.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/__init__.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/base.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/device_handlers.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/light_handlers.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/multizone_handlers.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/registry.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/handlers/tile_handlers.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/products/__init__.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/products/specs.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/__init__.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/base.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/const.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/generator.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/header.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/packets.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/protocol_types.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/protocol/serializer.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/repositories/__init__.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/repositories/device_repository.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/repositories/storage_backend.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/scenarios/__init__.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/scenarios/manager.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/scenarios/models.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/scenarios/persistence.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/src/lifx_emulator/server.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/conftest.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_api.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_api_validation.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_async_storage.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_backwards_compatibility.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_cli_validation.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_device.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_device_edge_cases.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_device_handlers_extended.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_device_manager.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_handler_registry.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_integration.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_light_handlers_extended.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_multizone_handlers_extended.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_observers.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_products_specs.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_protocol_generator.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_protocol_types_coverage.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_repositories.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_scenario_manager.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_scenario_persistence.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_serializer.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_server.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_state_restorer.py +0 -0
- {lifx_emulator-2.3.1 → lifx_emulator-2.4.0}/tests/test_tile_handlers_extended.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
|
|
@@ -130,6 +130,7 @@ lifx-emulator --help
|
|
|
130
130
|
- `--tile-count`: Tiles per device (uses product default if not specified)
|
|
131
131
|
- `--tile-width`: Width of each tile in zones (uses product default if not specified)
|
|
132
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)
|
|
@@ -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)
|
|
@@ -2,6 +2,14 @@
|
|
|
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
|
+
|
|
5
13
|
## v2.3.1 (2025-11-18)
|
|
6
14
|
|
|
7
15
|
### Bug Fixes
|
|
@@ -211,6 +211,16 @@ Height of each tile in zones. If not specified, uses product default (typically
|
|
|
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
|
|
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
|
|
@@ -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:
|
|
@@ -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,6 +286,7 @@ 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
292
|
tile_width: Width of each tile in zones. Uses product defaults if not
|
|
@@ -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
|
|
@@ -215,6 +215,35 @@ class EmulatedLifxDevice:
|
|
|
215
215
|
header.size = LIFX_HEADER_SIZE + payload_size
|
|
216
216
|
return header
|
|
217
217
|
|
|
218
|
+
def _should_handle_packet(self, pkt_type: int) -> bool:
|
|
219
|
+
"""Check if device should handle a packet type based on capabilities.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
pkt_type: Packet type number
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if device should handle, False if should return StateUnhandled
|
|
226
|
+
"""
|
|
227
|
+
# Device.* packets are always handled (2-59)
|
|
228
|
+
if 2 <= pkt_type <= 59:
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
# Light.* packets (101-149) require light capabilities
|
|
232
|
+
# Switches (devices with relays) don't support light operations
|
|
233
|
+
if 101 <= pkt_type <= 149:
|
|
234
|
+
return not self.state.has_relays
|
|
235
|
+
|
|
236
|
+
# MultiZone.* packets (501-512) require multizone capability
|
|
237
|
+
if 501 <= pkt_type <= 512:
|
|
238
|
+
return self.state.has_multizone
|
|
239
|
+
|
|
240
|
+
# Tile.* packets (701-720) require matrix capability
|
|
241
|
+
if 701 <= pkt_type <= 720:
|
|
242
|
+
return self.state.has_matrix
|
|
243
|
+
|
|
244
|
+
# Unknown packets - let handler decide
|
|
245
|
+
return True
|
|
246
|
+
|
|
218
247
|
def process_packet(
|
|
219
248
|
self, header: LifxHeader, packet: Any | None
|
|
220
249
|
) -> list[tuple[LifxHeader, Any]]:
|
|
@@ -229,6 +258,33 @@ class EmulatedLifxDevice:
|
|
|
229
258
|
logger.info("Dropping packet type %s per scenario", header.pkt_type)
|
|
230
259
|
return responses
|
|
231
260
|
|
|
261
|
+
# Check if device should handle this packet type (capability-based filtering)
|
|
262
|
+
if not self._should_handle_packet(header.pkt_type):
|
|
263
|
+
# Return StateUnhandled for unsupported packet types
|
|
264
|
+
state_unhandled = Device.StateUnhandled(unhandled_type=header.pkt_type)
|
|
265
|
+
unhandled_payload = state_unhandled.pack()
|
|
266
|
+
unhandled_header = self._create_response_header(
|
|
267
|
+
header.source,
|
|
268
|
+
header.sequence,
|
|
269
|
+
state_unhandled.PKT_TYPE,
|
|
270
|
+
len(unhandled_payload),
|
|
271
|
+
)
|
|
272
|
+
responses.append((unhandled_header, state_unhandled))
|
|
273
|
+
|
|
274
|
+
# Still send acknowledgment if requested
|
|
275
|
+
if header.ack_required:
|
|
276
|
+
ack_packet = Device.Acknowledgement()
|
|
277
|
+
ack_payload = ack_packet.pack()
|
|
278
|
+
ack_header = self._create_response_header(
|
|
279
|
+
header.source,
|
|
280
|
+
header.sequence,
|
|
281
|
+
ack_packet.PKT_TYPE,
|
|
282
|
+
len(ack_payload),
|
|
283
|
+
)
|
|
284
|
+
responses.append((ack_header, ack_packet))
|
|
285
|
+
|
|
286
|
+
return responses
|
|
287
|
+
|
|
232
288
|
# Update uptime
|
|
233
289
|
self.state.uptime_ns = self.get_uptime_ns()
|
|
234
290
|
|
|
@@ -183,6 +183,8 @@ class DeviceState:
|
|
|
183
183
|
has_extended_multizone: bool = False
|
|
184
184
|
has_matrix: bool = False
|
|
185
185
|
has_hev: bool = False
|
|
186
|
+
has_relays: bool = False
|
|
187
|
+
has_buttons: bool = False
|
|
186
188
|
|
|
187
189
|
# Attribute routing map: maps attribute prefixes to state objects
|
|
188
190
|
# This eliminates ~360 lines of property boilerplate
|
|
@@ -347,6 +349,8 @@ class DeviceState:
|
|
|
347
349
|
"has_extended_multizone",
|
|
348
350
|
"has_matrix",
|
|
349
351
|
"has_hev",
|
|
352
|
+
"has_relays",
|
|
353
|
+
"has_buttons",
|
|
350
354
|
} or name.startswith("_"):
|
|
351
355
|
object.__setattr__(self, name, value)
|
|
352
356
|
return
|
|
@@ -15,6 +15,7 @@ from lifx_emulator.factories.factory import (
|
|
|
15
15
|
create_hev_light,
|
|
16
16
|
create_infrared_light,
|
|
17
17
|
create_multizone_light,
|
|
18
|
+
create_switch,
|
|
18
19
|
create_tile_device,
|
|
19
20
|
)
|
|
20
21
|
from lifx_emulator.factories.firmware_config import FirmwareConfig
|
|
@@ -32,6 +33,7 @@ __all__ = [
|
|
|
32
33
|
"create_infrared_light",
|
|
33
34
|
"create_hev_light",
|
|
34
35
|
"create_multizone_light",
|
|
36
|
+
"create_switch",
|
|
35
37
|
"create_tile_device",
|
|
36
38
|
"create_color_temperature_light",
|
|
37
39
|
]
|
|
@@ -257,6 +257,8 @@ class DeviceBuilder:
|
|
|
257
257
|
has_extended_multizone=has_extended_multizone,
|
|
258
258
|
has_matrix=self._product_info.has_matrix,
|
|
259
259
|
has_hev=self._product_info.has_hev,
|
|
260
|
+
has_relays=self._product_info.has_relays,
|
|
261
|
+
has_buttons=self._product_info.has_buttons,
|
|
260
262
|
)
|
|
261
263
|
|
|
262
264
|
# 10. Restore saved state if persistence enabled
|
|
@@ -141,6 +141,37 @@ def create_color_temperature_light(
|
|
|
141
141
|
) # LIFX Mini White to Warm
|
|
142
142
|
|
|
143
143
|
|
|
144
|
+
def create_switch(
|
|
145
|
+
serial: str | None = None,
|
|
146
|
+
product_id: int = 70,
|
|
147
|
+
firmware_version: tuple[int, int] | None = None,
|
|
148
|
+
storage: DevicePersistenceAsyncFile | None = None,
|
|
149
|
+
scenario_manager: HierarchicalScenarioManager | None = None,
|
|
150
|
+
) -> EmulatedLifxDevice:
|
|
151
|
+
"""Create a LIFX Switch device.
|
|
152
|
+
|
|
153
|
+
Switches have has_relays and has_buttons capabilities but no lighting.
|
|
154
|
+
They respond with StateUnhandled (223) to Light, MultiZone, and Tile packets.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
serial: Device serial number (auto-generated if None)
|
|
158
|
+
product_id: Switch product ID (default: 70 - LIFX Switch)
|
|
159
|
+
firmware_version: Optional firmware version (major, minor)
|
|
160
|
+
storage: Optional persistence backend
|
|
161
|
+
scenario_manager: Optional scenario manager for testing
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
EmulatedLifxDevice configured as a switch
|
|
165
|
+
"""
|
|
166
|
+
return create_device(
|
|
167
|
+
product_id,
|
|
168
|
+
serial=serial,
|
|
169
|
+
firmware_version=firmware_version,
|
|
170
|
+
storage=storage,
|
|
171
|
+
scenario_manager=scenario_manager,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
144
175
|
def create_device(
|
|
145
176
|
product_id: int,
|
|
146
177
|
serial: str | None = None,
|
|
@@ -174,7 +174,6 @@ def generate_product_definitions(
|
|
|
174
174
|
code_lines.append("PRODUCTS: dict[int, ProductInfo] = {")
|
|
175
175
|
|
|
176
176
|
product_count = 0
|
|
177
|
-
skipped_count = 0
|
|
178
177
|
for vendor_data in all_vendors:
|
|
179
178
|
vendor_id = vendor_data.get("vid", 1)
|
|
180
179
|
defaults = vendor_data.get("defaults", {})
|
|
@@ -186,11 +185,6 @@ def generate_product_definitions(
|
|
|
186
185
|
name = product["name"]
|
|
187
186
|
features = {**default_features, **product.get("features", {})}
|
|
188
187
|
|
|
189
|
-
# Skip switch products (devices with relays) - these are not lights
|
|
190
|
-
if features.get("relays"):
|
|
191
|
-
skipped_count += 1
|
|
192
|
-
continue
|
|
193
|
-
|
|
194
188
|
# Build capabilities
|
|
195
189
|
capabilities = _build_capabilities(features)
|
|
196
190
|
|
|
@@ -223,8 +217,6 @@ def generate_product_definitions(
|
|
|
223
217
|
code_lines.append("")
|
|
224
218
|
|
|
225
219
|
print(f"Generated {product_count} product definitions")
|
|
226
|
-
if skipped_count > 0:
|
|
227
|
-
print(f"Skipped {skipped_count} switch products (relays only)")
|
|
228
220
|
return "\n".join(code_lines)
|
|
229
221
|
|
|
230
222
|
|
|
@@ -373,16 +365,17 @@ class ProductInfo:
|
|
|
373
365
|
"""Format product capabilities as a human-readable string.
|
|
374
366
|
|
|
375
367
|
Returns:
|
|
376
|
-
Comma-separated capability string (e.g., "
|
|
368
|
+
Comma-separated capability string (e.g., "color, infrared, multizone")
|
|
377
369
|
"""
|
|
378
370
|
caps = []
|
|
379
371
|
|
|
380
|
-
|
|
372
|
+
if self.has_buttons:
|
|
373
|
+
caps.append("buttons")
|
|
374
|
+
|
|
381
375
|
if self.has_relays:
|
|
382
|
-
|
|
383
|
-
caps.append("switch")
|
|
376
|
+
caps.append("relays")
|
|
384
377
|
elif self.has_color:
|
|
385
|
-
caps.append("
|
|
378
|
+
caps.append("color")
|
|
386
379
|
else:
|
|
387
380
|
# Check temperature range to determine white light type
|
|
388
381
|
if self.temperature_range:
|
|
@@ -409,9 +402,7 @@ class ProductInfo:
|
|
|
409
402
|
caps.append("HEV")
|
|
410
403
|
if self.has_chain:
|
|
411
404
|
caps.append("chain")
|
|
412
|
-
|
|
413
|
-
# Only show buttons if not already identified as switch
|
|
414
|
-
caps.append("buttons")
|
|
405
|
+
|
|
415
406
|
|
|
416
407
|
return ", ".join(caps) if caps else "unknown"
|
|
417
408
|
|
|
@@ -465,10 +456,6 @@ class ProductRegistry:
|
|
|
465
456
|
prod_features = product.get("features", {})
|
|
466
457
|
features: dict[str, Any] = {**default_features, **prod_features}
|
|
467
458
|
|
|
468
|
-
# Skip switch products (devices with relays) - these are not lights
|
|
469
|
-
if features.get("relays"):
|
|
470
|
-
continue
|
|
471
|
-
|
|
472
459
|
# Build capabilities bitfield
|
|
473
460
|
capabilities = 0
|
|
474
461
|
if features.get("color"):
|
|
@@ -667,7 +654,7 @@ def _discover_new_products(
|
|
|
667
654
|
products_data: dict[str, Any] | list[dict[str, Any]],
|
|
668
655
|
existing_specs: dict[int, dict[str, Any]],
|
|
669
656
|
) -> list[dict[str, Any]]:
|
|
670
|
-
"""Find new multizone or
|
|
657
|
+
"""Find new multizone, matrix, or switch products that need specs templates.
|
|
671
658
|
|
|
672
659
|
Args:
|
|
673
660
|
products_data: Parsed products.json data
|
|
@@ -691,21 +678,19 @@ def _discover_new_products(
|
|
|
691
678
|
pid = product["pid"]
|
|
692
679
|
features = {**default_features, **product.get("features", {})}
|
|
693
680
|
|
|
694
|
-
# Skip switch products (devices with relays) - these are not lights
|
|
695
|
-
if features.get("relays"):
|
|
696
|
-
continue
|
|
697
|
-
|
|
698
681
|
# Check if this product needs specs template
|
|
699
682
|
if pid not in existing_specs:
|
|
700
683
|
is_multizone = features.get("multizone", False)
|
|
701
684
|
is_matrix = features.get("matrix", False)
|
|
685
|
+
is_switch = features.get("relays", False)
|
|
702
686
|
|
|
703
|
-
if is_multizone or is_matrix:
|
|
687
|
+
if is_multizone or is_matrix or is_switch:
|
|
704
688
|
new_product = {
|
|
705
689
|
"pid": pid,
|
|
706
690
|
"name": product["name"],
|
|
707
691
|
"multizone": is_multizone,
|
|
708
692
|
"matrix": is_matrix,
|
|
693
|
+
"switch": is_switch,
|
|
709
694
|
"extended_multizone": False,
|
|
710
695
|
}
|
|
711
696
|
|
|
@@ -732,7 +717,12 @@ def _add_product_templates(
|
|
|
732
717
|
for product in new_products:
|
|
733
718
|
product_name = product["name"].replace('"', '\\"')
|
|
734
719
|
|
|
735
|
-
if product["
|
|
720
|
+
if product["switch"]:
|
|
721
|
+
existing_specs[product["pid"]] = {
|
|
722
|
+
"relay_count": 2,
|
|
723
|
+
"notes": product_name,
|
|
724
|
+
}
|
|
725
|
+
elif product["multizone"]:
|
|
736
726
|
existing_specs[product["pid"]] = {
|
|
737
727
|
"default_zone_count": 16,
|
|
738
728
|
"min_zone_count": 1,
|
|
@@ -752,28 +742,32 @@ def _add_product_templates(
|
|
|
752
742
|
|
|
753
743
|
def _categorize_products(
|
|
754
744
|
existing_specs: dict[int, dict[str, Any]],
|
|
755
|
-
) -> tuple[list[int], list[int]]:
|
|
756
|
-
"""Categorize products into multizone and matrix.
|
|
745
|
+
) -> tuple[list[int], list[int], list[int]]:
|
|
746
|
+
"""Categorize products into switch, multizone, and matrix.
|
|
757
747
|
|
|
758
748
|
Args:
|
|
759
749
|
existing_specs: Product specs dictionary
|
|
760
750
|
|
|
761
751
|
Returns:
|
|
762
|
-
Tuple of (sorted_multizone_pids, sorted_matrix_pids)
|
|
752
|
+
Tuple of (sorted_switch_pids, sorted_multizone_pids, sorted_matrix_pids)
|
|
763
753
|
"""
|
|
754
|
+
switch_pids = []
|
|
764
755
|
multizone_pids = []
|
|
765
756
|
matrix_pids = []
|
|
766
757
|
|
|
767
758
|
for pid, specs in existing_specs.items():
|
|
768
|
-
if "
|
|
759
|
+
if "relay_count" in specs:
|
|
760
|
+
switch_pids.append(pid)
|
|
761
|
+
elif "tile_width" in specs or "tile_height" in specs:
|
|
769
762
|
matrix_pids.append(pid)
|
|
770
763
|
elif "default_zone_count" in specs:
|
|
771
764
|
multizone_pids.append(pid)
|
|
772
765
|
|
|
766
|
+
switch_pids.sort()
|
|
773
767
|
multizone_pids.sort()
|
|
774
768
|
matrix_pids.sort()
|
|
775
769
|
|
|
776
|
-
return multizone_pids, matrix_pids
|
|
770
|
+
return switch_pids, multizone_pids, matrix_pids
|
|
777
771
|
|
|
778
772
|
|
|
779
773
|
def _generate_yaml_header() -> list[str]:
|
|
@@ -833,6 +827,53 @@ def _generate_yaml_header() -> list[str]:
|
|
|
833
827
|
]
|
|
834
828
|
|
|
835
829
|
|
|
830
|
+
def _generate_switch_section(
|
|
831
|
+
switch_pids: list[int], existing_specs: dict[int, dict[str, Any]]
|
|
832
|
+
) -> list[str]:
|
|
833
|
+
"""Generate YAML lines for switch products section.
|
|
834
|
+
|
|
835
|
+
Args:
|
|
836
|
+
switch_pids: Sorted list of switch product IDs
|
|
837
|
+
existing_specs: Product specs dictionary
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
List of YAML lines
|
|
841
|
+
"""
|
|
842
|
+
if not switch_pids:
|
|
843
|
+
return []
|
|
844
|
+
|
|
845
|
+
lines = [
|
|
846
|
+
" # ========================================",
|
|
847
|
+
" # Switch Products (Relays)",
|
|
848
|
+
" # ========================================",
|
|
849
|
+
"",
|
|
850
|
+
]
|
|
851
|
+
|
|
852
|
+
for pid in switch_pids:
|
|
853
|
+
specs = existing_specs[pid]
|
|
854
|
+
name = specs.get("notes", f"Product {pid}").split(" - ")[0]
|
|
855
|
+
|
|
856
|
+
lines.append(f" {pid}: # {name}")
|
|
857
|
+
lines.append(f" relay_count: {specs['relay_count']}")
|
|
858
|
+
|
|
859
|
+
# Add firmware version if present
|
|
860
|
+
if "default_firmware_major" in specs and "default_firmware_minor" in specs:
|
|
861
|
+
lines.append(
|
|
862
|
+
f" default_firmware_major: {specs['default_firmware_major']}"
|
|
863
|
+
)
|
|
864
|
+
lines.append(
|
|
865
|
+
f" default_firmware_minor: {specs['default_firmware_minor']}"
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
notes = specs.get("notes", "")
|
|
869
|
+
if notes:
|
|
870
|
+
notes_escaped = notes.replace('"', '\\"')
|
|
871
|
+
lines.append(f' notes: "{notes_escaped}"')
|
|
872
|
+
lines.append("")
|
|
873
|
+
|
|
874
|
+
return lines
|
|
875
|
+
|
|
876
|
+
|
|
836
877
|
def _generate_multizone_section(
|
|
837
878
|
multizone_pids: list[int], existing_specs: dict[int, dict[str, Any]]
|
|
838
879
|
) -> list[str]:
|
|
@@ -964,12 +1005,13 @@ def update_specs_file(
|
|
|
964
1005
|
_add_product_templates(new_products, existing_specs)
|
|
965
1006
|
|
|
966
1007
|
# Categorize products and sort
|
|
967
|
-
multizone_pids, matrix_pids = _categorize_products(existing_specs)
|
|
1008
|
+
switch_pids, multizone_pids, matrix_pids = _categorize_products(existing_specs)
|
|
968
1009
|
|
|
969
1010
|
# Build YAML content
|
|
970
1011
|
lines = _generate_yaml_header()
|
|
971
1012
|
lines.extend(_generate_multizone_section(multizone_pids, existing_specs))
|
|
972
1013
|
lines.extend(_generate_matrix_section(matrix_pids, existing_specs))
|
|
1014
|
+
lines.extend(_generate_switch_section(switch_pids, existing_specs))
|
|
973
1015
|
|
|
974
1016
|
# Write the new file
|
|
975
1017
|
with open(specs_path, "w") as f:
|