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.
Files changed (156) hide show
  1. {lifx_async-4.6.1 → lifx_async-4.7.0}/CLAUDE.md +16 -0
  2. {lifx_async-4.6.1 → lifx_async-4.7.0}/PKG-INFO +1 -1
  3. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/changelog.md +8 -0
  4. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/advanced-usage.md +43 -0
  5. {lifx_async-4.6.1 → lifx_async-4.7.0}/pyproject.toml +1 -1
  6. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/multizone.py +33 -12
  7. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_multizone.py +38 -0
  8. {lifx_async-4.6.1 → lifx_async-4.7.0}/uv.lock +1 -1
  9. {lifx_async-4.6.1 → lifx_async-4.7.0}/.claude/settings.json +0 -0
  10. {lifx_async-4.6.1 → lifx_async-4.7.0}/.github/dependabot.yml +0 -0
  11. {lifx_async-4.6.1 → lifx_async-4.7.0}/.github/labeler.yml +0 -0
  12. {lifx_async-4.6.1 → lifx_async-4.7.0}/.github/workflows/ci.yml +0 -0
  13. {lifx_async-4.6.1 → lifx_async-4.7.0}/.github/workflows/docs.yml +0 -0
  14. {lifx_async-4.6.1 → lifx_async-4.7.0}/.github/workflows/pr-automation.yml +0 -0
  15. {lifx_async-4.6.1 → lifx_async-4.7.0}/.gitignore +0 -0
  16. {lifx_async-4.6.1 → lifx_async-4.7.0}/.pre-commit-config.yaml +0 -0
  17. {lifx_async-4.6.1 → lifx_async-4.7.0}/LICENSE +0 -0
  18. {lifx_async-4.6.1 → lifx_async-4.7.0}/README.md +0 -0
  19. {lifx_async-4.6.1 → lifx_async-4.7.0}/context7.json +0 -0
  20. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/colors.md +0 -0
  21. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/devices.md +0 -0
  22. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/effects.md +0 -0
  23. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/exceptions.md +0 -0
  24. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/high-level.md +0 -0
  25. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/index.md +0 -0
  26. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/network.md +0 -0
  27. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/protocol.md +0 -0
  28. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/api/themes.md +0 -0
  29. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/architecture/effects-architecture.md +0 -0
  30. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/architecture/overview.md +0 -0
  31. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/faq.md +0 -0
  32. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/getting-started/effects.md +0 -0
  33. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/getting-started/installation.md +0 -0
  34. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/getting-started/quickstart.md +0 -0
  35. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/getting-started/themes.md +0 -0
  36. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/index.md +0 -0
  37. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/migration/effect-api-changes.md +0 -0
  38. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/stylesheets/extra.css +0 -0
  39. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/ceiling-lights.md +0 -0
  40. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/effects-custom.md +0 -0
  41. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/effects-troubleshooting.md +0 -0
  42. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/protocol-deep-dive.md +0 -0
  43. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/themes.md +0 -0
  44. {lifx_async-4.6.1 → lifx_async-4.7.0}/docs/user-guide/troubleshooting.md +0 -0
  45. {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/01_simple_discovery.py +0 -0
  46. {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/02_simple_control.py +0 -0
  47. {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/03_waveforms.py +0 -0
  48. {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/04_logging.py +0 -0
  49. {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/06_pulse_effect.py +0 -0
  50. {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/07_colorloop_effect.py +0 -0
  51. {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/08_custom_effect.py +0 -0
  52. {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/09_background_effect.py +0 -0
  53. {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/10_find_specific_devices.py +0 -0
  54. {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/11_matrix_basic.py +0 -0
  55. {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/12_matrix_effects.py +0 -0
  56. {lifx_async-4.6.1 → lifx_async-4.7.0}/examples/13_matrix_large.py +0 -0
  57. {lifx_async-4.6.1 → lifx_async-4.7.0}/mkdocs.yml +0 -0
  58. {lifx_async-4.6.1 → lifx_async-4.7.0}/renovate.json +0 -0
  59. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/__init__.py +0 -0
  60. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/api.py +0 -0
  61. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/color.py +0 -0
  62. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/const.py +0 -0
  63. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/__init__.py +0 -0
  64. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/base.py +0 -0
  65. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/ceiling.py +0 -0
  66. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/hev.py +0 -0
  67. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/infrared.py +0 -0
  68. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/light.py +0 -0
  69. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/devices/matrix.py +0 -0
  70. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/__init__.py +0 -0
  71. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/base.py +0 -0
  72. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/colorloop.py +0 -0
  73. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/conductor.py +0 -0
  74. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/const.py +0 -0
  75. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/models.py +0 -0
  76. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/pulse.py +0 -0
  77. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/effects/state_manager.py +0 -0
  78. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/exceptions.py +0 -0
  79. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/network/__init__.py +0 -0
  80. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/network/connection.py +0 -0
  81. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/network/discovery.py +0 -0
  82. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/network/message.py +0 -0
  83. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/network/transport.py +0 -0
  84. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/products/__init__.py +0 -0
  85. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/products/generator.py +0 -0
  86. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/products/quirks.py +0 -0
  87. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/products/registry.py +0 -0
  88. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/__init__.py +0 -0
  89. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/base.py +0 -0
  90. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/generator.py +0 -0
  91. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/header.py +0 -0
  92. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/models.py +0 -0
  93. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/packets.py +0 -0
  94. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/protocol_types.py +0 -0
  95. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/protocol/serializer.py +0 -0
  96. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/py.typed +0 -0
  97. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/theme/__init__.py +0 -0
  98. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/theme/canvas.py +0 -0
  99. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/theme/generators.py +0 -0
  100. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/theme/library.py +0 -0
  101. {lifx_async-4.6.1 → lifx_async-4.7.0}/src/lifx/theme/theme.py +0 -0
  102. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/__init__.py +0 -0
  103. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/conftest.py +0 -0
  104. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_api/__init__.py +0 -0
  105. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_api/test_api_apply_theme.py +0 -0
  106. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_api/test_api_batch_errors.py +0 -0
  107. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_api/test_api_batch_operations.py +0 -0
  108. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_api/test_api_discovery.py +0 -0
  109. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_api/test_api_organization.py +0 -0
  110. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_color.py +0 -0
  111. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/__init__.py +0 -0
  112. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/conftest.py +0 -0
  113. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_base.py +0 -0
  114. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_ceiling.py +0 -0
  115. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_hev.py +0 -0
  116. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_infrared.py +0 -0
  117. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_light.py +0 -0
  118. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_mac_address.py +0 -0
  119. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_matrix.py +0 -0
  120. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_ceiling.py +0 -0
  121. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_hev.py +0 -0
  122. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_infrared.py +0 -0
  123. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_light.py +0 -0
  124. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_management.py +0 -0
  125. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_matrix.py +0 -0
  126. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_devices/test_state_multizone.py +0 -0
  127. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/__init__.py +0 -0
  128. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_base.py +0 -0
  129. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_capability_filtering.py +0 -0
  130. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_colorloop.py +0 -0
  131. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_integration.py +0 -0
  132. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_models.py +0 -0
  133. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_pulse.py +0 -0
  134. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_effects/test_state_manager.py +0 -0
  135. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/__init__.py +0 -0
  136. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_concurrent_requests.py +0 -0
  137. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_connection.py +0 -0
  138. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_discovery_devices.py +0 -0
  139. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_discovery_errors.py +0 -0
  140. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_message.py +0 -0
  141. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_message_advanced.py +0 -0
  142. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_network/test_transport.py +0 -0
  143. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_products/test_product_generator.py +0 -0
  144. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_products/test_registry.py +0 -0
  145. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_protocol/test_generated.py +0 -0
  146. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_protocol/test_header.py +0 -0
  147. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_protocol/test_protocol_generator.py +0 -0
  148. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_protocol/test_serializer.py +0 -0
  149. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/__init__.py +0 -0
  150. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/conftest.py +0 -0
  151. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/test_apply_theme.py +0 -0
  152. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/test_canvas.py +0 -0
  153. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/test_generators.py +0 -0
  154. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/test_library.py +0 -0
  155. {lifx_async-4.6.1 → lifx_async-4.7.0}/tests/test_theme/test_theme.py +0 -0
  156. {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`)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-async
3
- Version: 4.6.1
3
+ Version: 4.7.0
4
4
  Summary: A modern, type-safe, async Python library for controlling LIFX lights
5
5
  Author-email: Avi Miller <me@dje.li>
6
6
  Maintainer-email: Avi Miller <me@dje.li>
@@ -2,6 +2,14 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## 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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lifx-async"
3
- version = "4.6.1"
3
+ version = "4.7.0"
4
4
  description = "A modern, type-safe, async Python library for controlling LIFX lights"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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") # Convert to protocol HSBK
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
- # Send request
651
- result = await self.connection.request(
652
- packets.MultiZone.SetExtendedColorZones(
653
- duration=duration_ms,
654
- apply=apply,
655
- index=zone_index,
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
- self._raise_if_unhandled(result)
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."""
@@ -337,7 +337,7 @@ wheels = [
337
337
 
338
338
  [[package]]
339
339
  name = "lifx-async"
340
- version = "4.6.1"
340
+ version = "4.7.0"
341
341
  source = { editable = "." }
342
342
 
343
343
  [package.dev-dependencies]
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