lifx-async 4.6.1__tar.gz → 4.7.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_async-4.6.1 → lifx_async-4.7.0}/CLAUDE.md +16 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/PKG-INFO +1 -1
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/changelog.md +8 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/advanced-usage.md +43 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/pyproject.toml +1 -1
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/multizone.py +33 -12
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_multizone.py +38 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/uv.lock +1 -1
- {lifx_async-4.6.1 → lifx_async-4.7.0}/.claude/settings.json +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/.github/dependabot.yml +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/.github/labeler.yml +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/.github/workflows/ci.yml +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/.github/workflows/docs.yml +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/.gitignore +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/.pre-commit-config.yaml +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/LICENSE +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/README.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/context7.json +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/colors.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/devices.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/effects.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/exceptions.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/high-level.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/index.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/network.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/protocol.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/themes.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/architecture/overview.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/faq.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/getting-started/effects.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/getting-started/installation.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/getting-started/themes.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/index.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/stylesheets/extra.css +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/ceiling-lights.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/themes.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/01_simple_discovery.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/02_simple_control.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/03_waveforms.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/04_logging.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/06_pulse_effect.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/08_custom_effect.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/09_background_effect.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/11_matrix_basic.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/12_matrix_effects.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/13_matrix_large.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/mkdocs.yml +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/renovate.json +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/api.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/color.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/const.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/base.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/ceiling.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/hev.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/infrared.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/light.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/matrix.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/base.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/colorloop.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/const.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/models.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/exceptions.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/network/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/network/connection.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/network/discovery.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/network/message.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/network/transport.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/products/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/products/generator.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/products/quirks.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/products/registry.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/base.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/generator.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/header.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/models.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/protocol_types.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/py.typed +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/theme/generators.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/theme/library.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/theme/theme.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/conftest.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_api/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_api/test_api_apply_theme.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_api/test_api_batch_operations.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_api/test_api_discovery.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_color.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/conftest.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_base.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_ceiling.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_light.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_matrix.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_ceiling.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_hev.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_infrared.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_light.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_management.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_matrix.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_multizone.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_base.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_models.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_concurrent_requests.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_connection.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_message.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_transport.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_products/test_registry.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_protocol/test_serializer.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/__init__.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/conftest.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/test_canvas.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/test_library.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_utils.py +0 -0
|
@@ -459,6 +459,22 @@ async with await MultiZoneLight.from_ip("192.168.1.100") as light:
|
|
|
459
459
|
- `get_extended_color_zones(start, end)`: Direct access to extended multizone protocol (requires extended capability)
|
|
460
460
|
- `get_color_zones(start, end)`: Direct access to standard multizone protocol (works on all multizone devices)
|
|
461
461
|
|
|
462
|
+
**Fire-and-forget mode for animations:**
|
|
463
|
+
|
|
464
|
+
For high-frequency animations (>20 updates/second), use the `fast=True` parameter to skip waiting for device acknowledgement:
|
|
465
|
+
|
|
466
|
+
```python
|
|
467
|
+
# Standard mode (waits for response)
|
|
468
|
+
await light.set_extended_color_zones(0, colors)
|
|
469
|
+
|
|
470
|
+
# Fast mode for animations (fire-and-forget, no response waiting)
|
|
471
|
+
for frame in animation_frames:
|
|
472
|
+
await light.set_extended_color_zones(0, frame, fast=True)
|
|
473
|
+
await asyncio.sleep(0.033) # ~30 FPS
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Note:** `MatrixLight.set64()` is already fire-and-forget by default.
|
|
477
|
+
|
|
462
478
|
### Packet Flow
|
|
463
479
|
|
|
464
480
|
1. Create packet instance (e.g., `LightSetColor`)
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v4.7.0 (2025-12-13)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **devices**: Add fast parameter to set_extended_color_zones()
|
|
10
|
+
([`0276fca`](https://github.com/Djelibeybi/lifx-async/commit/0276fca9b18e9f78441c843880ef52b4c79dac7b))
|
|
11
|
+
|
|
12
|
+
|
|
5
13
|
## v4.6.1 (2025-12-12)
|
|
6
14
|
|
|
7
15
|
### Bug Fixes
|
|
@@ -479,6 +479,49 @@ async def with_reuse():
|
|
|
479
479
|
# Connection closed once at end
|
|
480
480
|
```
|
|
481
481
|
|
|
482
|
+
### Fire-and-Forget Mode for High-Frequency Animations
|
|
483
|
+
|
|
484
|
+
For animations sending more than 20 updates per second, waiting for device acknowledgement creates unacceptable latency. Use the `fast=True` parameter to enable fire-and-forget mode:
|
|
485
|
+
|
|
486
|
+
```python
|
|
487
|
+
import asyncio
|
|
488
|
+
from lifx import MultiZoneLight, HSBK
|
|
489
|
+
|
|
490
|
+
async def rainbow_animation():
|
|
491
|
+
async with await MultiZoneLight.from_ip("192.168.1.100") as light:
|
|
492
|
+
zone_count = await light.get_zone_count()
|
|
493
|
+
|
|
494
|
+
# Animation loop at ~30 FPS
|
|
495
|
+
offset = 0
|
|
496
|
+
while True:
|
|
497
|
+
# Generate rainbow colors
|
|
498
|
+
colors = [
|
|
499
|
+
HSBK(hue=(i * 360 / zone_count + offset) % 360,
|
|
500
|
+
saturation=1.0, brightness=1.0, kelvin=3500)
|
|
501
|
+
for i in range(zone_count)
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
# Fire-and-forget: no waiting for response
|
|
505
|
+
await light.set_extended_color_zones(0, colors, fast=True)
|
|
506
|
+
|
|
507
|
+
offset = (offset + 5) % 360
|
|
508
|
+
await asyncio.sleep(0.033) # ~30 FPS
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**When to use `fast=True`:**
|
|
512
|
+
|
|
513
|
+
- High-frequency animations (>20 updates/second)
|
|
514
|
+
- Real-time visualizations (music sync, games)
|
|
515
|
+
- Smooth color transitions requiring rapid updates
|
|
516
|
+
|
|
517
|
+
**Trade-offs:**
|
|
518
|
+
|
|
519
|
+
- No confirmation that the device received or applied the colors
|
|
520
|
+
- No error detection (timeouts, unsupported commands)
|
|
521
|
+
- Best for visual effects where occasional dropped frames are acceptable
|
|
522
|
+
|
|
523
|
+
**Note:** `MatrixLight.set64()` is already fire-and-forget by default, making it ideal for tile animations without any additional parameters.
|
|
524
|
+
|
|
482
525
|
## Next Steps
|
|
483
526
|
|
|
484
527
|
- [Troubleshooting Guide](troubleshooting.md) - Common issues and solutions
|
|
@@ -604,6 +604,8 @@ class MultiZoneLight(Light):
|
|
|
604
604
|
colors: list[HSBK],
|
|
605
605
|
duration: float = 0.0,
|
|
606
606
|
apply: ExtendedAppReq = ExtendedAppReq.APPLY,
|
|
607
|
+
*,
|
|
608
|
+
fast: bool = False,
|
|
607
609
|
) -> None:
|
|
608
610
|
"""Set colors for multiple zones efficiently (up to 82 zones per call).
|
|
609
611
|
|
|
@@ -615,12 +617,15 @@ class MultiZoneLight(Light):
|
|
|
615
617
|
colors: List of HSBK colors to set (max 82)
|
|
616
618
|
duration: Transition duration in seconds (default 0.0)
|
|
617
619
|
apply: Application mode (default APPLY)
|
|
620
|
+
fast: If True, send fire-and-forget without waiting for response.
|
|
621
|
+
Use for high-frequency animations (>20 updates/second).
|
|
618
622
|
|
|
619
623
|
Raises:
|
|
620
624
|
ValueError: If colors list is too long or zone index is invalid
|
|
621
625
|
LifxDeviceNotFoundError: If device is not connected
|
|
622
|
-
LifxTimeoutError: If device does not respond
|
|
626
|
+
LifxTimeoutError: If device does not respond (only when fast=False)
|
|
623
627
|
LifxUnsupportedCommandError: If device doesn't support this command
|
|
628
|
+
(only when fast=False)
|
|
624
629
|
|
|
625
630
|
Example:
|
|
626
631
|
```python
|
|
@@ -630,6 +635,11 @@ class MultiZoneLight(Light):
|
|
|
630
635
|
for i in range(10)
|
|
631
636
|
]
|
|
632
637
|
await light.set_extended_color_zones(0, colors)
|
|
638
|
+
|
|
639
|
+
# High-speed animation loop
|
|
640
|
+
for frame in animation_frames:
|
|
641
|
+
await light.set_extended_color_zones(0, frame, fast=True)
|
|
642
|
+
await asyncio.sleep(0.033) # ~30 FPS
|
|
633
643
|
```
|
|
634
644
|
"""
|
|
635
645
|
if zone_index < 0:
|
|
@@ -637,7 +647,9 @@ class MultiZoneLight(Light):
|
|
|
637
647
|
if len(colors) > 82:
|
|
638
648
|
raise ValueError(f"Too many colors: {len(colors)} (max 82 per request)")
|
|
639
649
|
if len(colors) == 0:
|
|
640
|
-
raise ValueError("Colors list cannot be empty")
|
|
650
|
+
raise ValueError("Colors list cannot be empty")
|
|
651
|
+
|
|
652
|
+
# Convert to protocol HSBK
|
|
641
653
|
protocol_colors = [color.to_protocol() for color in colors]
|
|
642
654
|
|
|
643
655
|
# Pad to 82 colors if needed
|
|
@@ -647,17 +659,25 @@ class MultiZoneLight(Light):
|
|
|
647
659
|
# Convert duration to milliseconds
|
|
648
660
|
duration_ms = int(duration * 1000)
|
|
649
661
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
colors_count=len(colors),
|
|
657
|
-
colors=protocol_colors,
|
|
658
|
-
),
|
|
662
|
+
packet = packets.MultiZone.SetExtendedColorZones(
|
|
663
|
+
duration=duration_ms,
|
|
664
|
+
apply=apply,
|
|
665
|
+
index=zone_index,
|
|
666
|
+
colors_count=len(colors),
|
|
667
|
+
colors=protocol_colors,
|
|
659
668
|
)
|
|
660
|
-
|
|
669
|
+
|
|
670
|
+
if fast:
|
|
671
|
+
# Fire-and-forget: no ack, no response, no waiting
|
|
672
|
+
await self.connection.send_packet(
|
|
673
|
+
packet,
|
|
674
|
+
ack_required=False,
|
|
675
|
+
res_required=False,
|
|
676
|
+
)
|
|
677
|
+
else:
|
|
678
|
+
# Standard: wait for response and check for errors
|
|
679
|
+
result = await self.connection.request(packet)
|
|
680
|
+
self._raise_if_unhandled(result)
|
|
661
681
|
|
|
662
682
|
_LOGGER.debug(
|
|
663
683
|
{
|
|
@@ -678,6 +698,7 @@ class MultiZoneLight(Light):
|
|
|
678
698
|
],
|
|
679
699
|
"duration": duration_ms,
|
|
680
700
|
"apply": apply.name,
|
|
701
|
+
"fast": fast,
|
|
681
702
|
},
|
|
682
703
|
}
|
|
683
704
|
)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import AsyncIterator
|
|
6
|
+
from unittest.mock import AsyncMock
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
8
9
|
|
|
@@ -429,6 +430,43 @@ class TestMultiZoneLight:
|
|
|
429
430
|
with pytest.raises(ValueError, match="Too many colors"):
|
|
430
431
|
await multizone_light.set_extended_color_zones(0, colors)
|
|
431
432
|
|
|
433
|
+
async def test_set_extended_color_zones_fast_mode(
|
|
434
|
+
self, multizone_light: MultiZoneLight
|
|
435
|
+
) -> None:
|
|
436
|
+
"""Test setting extended color zones in fast (fire-and-forget) mode."""
|
|
437
|
+
# Pre-populate zone count to avoid internal get_zone_count() calls
|
|
438
|
+
multizone_light._zone_count = 82
|
|
439
|
+
|
|
440
|
+
# Set up send_packet as AsyncMock for fire-and-forget mode
|
|
441
|
+
multizone_light.connection.send_packet = AsyncMock()
|
|
442
|
+
|
|
443
|
+
# Create list of colors
|
|
444
|
+
colors = [
|
|
445
|
+
HSBK(hue=i * 36, saturation=1.0, brightness=1.0, kelvin=3500)
|
|
446
|
+
for i in range(10)
|
|
447
|
+
]
|
|
448
|
+
await multizone_light.set_extended_color_zones(
|
|
449
|
+
0, colors, duration=0.5, fast=True
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Verify send_packet was called (not request)
|
|
453
|
+
multizone_light.connection.send_packet.assert_called_once()
|
|
454
|
+
multizone_light.connection.request.assert_not_called()
|
|
455
|
+
|
|
456
|
+
# Get the send_packet call
|
|
457
|
+
call_args = multizone_light.connection.send_packet.call_args
|
|
458
|
+
|
|
459
|
+
# Verify packet has correct values
|
|
460
|
+
packet = call_args[0][0]
|
|
461
|
+
assert packet.index == 0
|
|
462
|
+
assert packet.colors_count == 10
|
|
463
|
+
assert packet.duration == 500 # 0.5 seconds in ms
|
|
464
|
+
assert len(packet.colors) == 82 # Padded to 82
|
|
465
|
+
|
|
466
|
+
# Verify fire-and-forget flags
|
|
467
|
+
assert call_args[1]["ack_required"] is False
|
|
468
|
+
assert call_args[1]["res_required"] is False
|
|
469
|
+
|
|
432
470
|
|
|
433
471
|
class TestMultiZoneEffect:
|
|
434
472
|
"""Tests for MultiZoneEffect class."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|