lifx-async 4.7.4__tar.gz → 4.7.5__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.7.4 → lifx_async-4.7.5}/PKG-INFO +1 -1
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/changelog.md +8 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/pyproject.toml +1 -1
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/devices/ceiling.py +75 -17
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_ceiling.py +32 -6
- {lifx_async-4.7.4 → lifx_async-4.7.5}/uv.lock +1 -1
- {lifx_async-4.7.4 → lifx_async-4.7.5}/.claude/settings.json +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/.github/dependabot.yml +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/.github/labeler.yml +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/.github/workflows/ci.yml +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/.github/workflows/docs.yml +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/.gitignore +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/.pre-commit-config.yaml +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/CLAUDE.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/LICENSE +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/README.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/context7.json +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/api/colors.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/api/devices.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/api/effects.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/api/exceptions.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/api/high-level.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/api/index.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/api/network.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/api/protocol.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/api/themes.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/architecture/overview.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/faq.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/getting-started/effects.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/getting-started/installation.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/getting-started/themes.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/index.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/stylesheets/extra.css +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/user-guide/advanced-usage.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/user-guide/ceiling-lights.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/user-guide/themes.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/examples/01_simple_discovery.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/examples/02_simple_control.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/examples/03_waveforms.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/examples/04_logging.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/examples/06_pulse_effect.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/examples/08_custom_effect.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/examples/09_background_effect.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/examples/11_matrix_basic.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/examples/12_matrix_effects.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/examples/13_matrix_large.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/mkdocs.yml +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/renovate.json +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/api.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/color.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/const.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/devices/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/devices/base.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/devices/hev.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/devices/infrared.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/devices/light.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/devices/matrix.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/devices/multizone.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/effects/base.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/effects/colorloop.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/effects/const.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/effects/models.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/exceptions.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/network/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/network/connection.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/network/discovery.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/network/message.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/network/transport.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/products/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/products/generator.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/products/quirks.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/products/registry.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/protocol/base.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/protocol/generator.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/protocol/header.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/protocol/models.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/protocol/protocol_types.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/py.typed +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/theme/generators.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/theme/library.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/src/lifx/theme/theme.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/conftest.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_api/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_api/test_api_apply_theme.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_api/test_api_batch_operations.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_api/test_api_discovery.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_color.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/conftest.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_base.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_light.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_matrix.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_multizone.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_state_ceiling.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_state_hev.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_state_infrared.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_state_light.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_state_management.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_state_matrix.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_devices/test_state_multizone.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_effects/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_effects/test_base.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_effects/test_models.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_network/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_network/test_concurrent_requests.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_network/test_connection.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_network/test_message.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_network/test_transport.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_products/test_registry.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_protocol/test_serializer.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_theme/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_theme/conftest.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_theme/test_canvas.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_theme/test_library.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.7.5}/tests/test_utils.py +0 -0
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v4.7.5 (2025-12-16)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **devices**: Override set_color in CeilingLight to track component state
|
|
10
|
+
([`0d20563`](https://github.com/Djelibeybi/lifx-async/commit/0d20563c170363229ab17620398283bd85ee7829))
|
|
11
|
+
|
|
12
|
+
|
|
5
13
|
## v4.7.4 (2025-12-16)
|
|
6
14
|
|
|
7
15
|
### Performance Improvements
|
|
@@ -347,6 +347,22 @@ class CeilingLight(MatrixLight):
|
|
|
347
347
|
|
|
348
348
|
return layout.downlight_zones
|
|
349
349
|
|
|
350
|
+
@property
|
|
351
|
+
def downlight_zone_count(self) -> int:
|
|
352
|
+
"""Number of downlight zones.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Zone count (63 for standard 8x8, 127 for Capsule 16x8)
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
LifxError: If device version is not available or not a Ceiling product
|
|
359
|
+
"""
|
|
360
|
+
# downlight_zones is slice(0, N), so stop equals the count
|
|
361
|
+
stop = self.downlight_zones.stop
|
|
362
|
+
if stop is None:
|
|
363
|
+
raise LifxError("Invalid downlight zones configuration")
|
|
364
|
+
return stop
|
|
365
|
+
|
|
350
366
|
@property
|
|
351
367
|
def uplight_is_on(self) -> bool:
|
|
352
368
|
"""True if uplight component is currently on.
|
|
@@ -496,7 +512,7 @@ class CeilingLight(MatrixLight):
|
|
|
496
512
|
"Cannot set downlight color with brightness=0. "
|
|
497
513
|
"Use turn_downlight_off() instead."
|
|
498
514
|
)
|
|
499
|
-
downlight_colors = [colors] *
|
|
515
|
+
downlight_colors = [colors] * self.downlight_zone_count
|
|
500
516
|
else:
|
|
501
517
|
if all(c.brightness == 0 for c in colors):
|
|
502
518
|
raise ValueError(
|
|
@@ -504,10 +520,10 @@ class CeilingLight(MatrixLight):
|
|
|
504
520
|
"Use turn_downlight_off() instead."
|
|
505
521
|
)
|
|
506
522
|
|
|
507
|
-
|
|
508
|
-
if len(colors) != expected_count:
|
|
523
|
+
if len(colors) != self.downlight_zone_count:
|
|
509
524
|
raise ValueError(
|
|
510
|
-
f"Expected {
|
|
525
|
+
f"Expected {self.downlight_zone_count} colors for downlight, "
|
|
526
|
+
f"got {len(colors)}"
|
|
511
527
|
)
|
|
512
528
|
downlight_colors = colors
|
|
513
529
|
|
|
@@ -684,10 +700,6 @@ class CeilingLight(MatrixLight):
|
|
|
684
700
|
ValueError: If list length doesn't match downlight zone count
|
|
685
701
|
LifxTimeoutError: Device did not respond
|
|
686
702
|
"""
|
|
687
|
-
# Number of downlight zones equals the uplight zone index
|
|
688
|
-
# (downlight is zones 0 to uplight_zone-1)
|
|
689
|
-
downlight_zone_count = self.uplight_zone
|
|
690
|
-
|
|
691
703
|
# Validate provided colors early
|
|
692
704
|
if colors is not None:
|
|
693
705
|
if isinstance(colors, HSBK):
|
|
@@ -696,9 +708,9 @@ class CeilingLight(MatrixLight):
|
|
|
696
708
|
else:
|
|
697
709
|
if all(c.brightness == 0 for c in colors):
|
|
698
710
|
raise ValueError("Cannot turn on downlight with brightness=0")
|
|
699
|
-
if len(colors) != downlight_zone_count:
|
|
711
|
+
if len(colors) != self.downlight_zone_count:
|
|
700
712
|
raise ValueError(
|
|
701
|
-
f"Expected {downlight_zone_count} colors for downlight, "
|
|
713
|
+
f"Expected {self.downlight_zone_count} colors for downlight, "
|
|
702
714
|
f"got {len(colors)}"
|
|
703
715
|
)
|
|
704
716
|
|
|
@@ -711,7 +723,7 @@ class CeilingLight(MatrixLight):
|
|
|
711
723
|
# Determine target colors (pass pre-fetched colors to avoid extra fetch)
|
|
712
724
|
if colors is not None:
|
|
713
725
|
if isinstance(colors, HSBK):
|
|
714
|
-
target_colors = [colors] * downlight_zone_count
|
|
726
|
+
target_colors = [colors] * self.downlight_zone_count
|
|
715
727
|
else:
|
|
716
728
|
target_colors = list(colors)
|
|
717
729
|
else:
|
|
@@ -750,7 +762,7 @@ class CeilingLight(MatrixLight):
|
|
|
750
762
|
# Light is already on - determine target colors first, then set
|
|
751
763
|
if colors is not None:
|
|
752
764
|
if isinstance(colors, HSBK):
|
|
753
|
-
target_colors = [colors] * downlight_zone_count
|
|
765
|
+
target_colors = [colors] * self.downlight_zone_count
|
|
754
766
|
else:
|
|
755
767
|
target_colors = list(colors)
|
|
756
768
|
else:
|
|
@@ -824,6 +836,54 @@ class CeilingLight(MatrixLight):
|
|
|
824
836
|
if turning_off and self._state_file:
|
|
825
837
|
self._save_state_to_file()
|
|
826
838
|
|
|
839
|
+
async def set_color(self, color: HSBK, duration: float = 0.0) -> None:
|
|
840
|
+
"""Set light color, updating component state tracking.
|
|
841
|
+
|
|
842
|
+
Overrides Light.set_color() to track the color change in the ceiling
|
|
843
|
+
light's component state. When set_color() is called, all zones (uplight
|
|
844
|
+
and downlight) are set to the same color. This override ensures that
|
|
845
|
+
the cached component colors stay in sync so that subsequent component
|
|
846
|
+
control methods (like turn_uplight_on or turn_downlight_on) use the
|
|
847
|
+
correct color values.
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
color: HSBK color to set for the entire light
|
|
851
|
+
duration: Transition duration in seconds (default 0.0)
|
|
852
|
+
|
|
853
|
+
Raises:
|
|
854
|
+
LifxDeviceNotFoundError: If device is not connected
|
|
855
|
+
LifxTimeoutError: If device does not respond
|
|
856
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
857
|
+
|
|
858
|
+
Example:
|
|
859
|
+
```python
|
|
860
|
+
from lifx.color import HSBK
|
|
861
|
+
|
|
862
|
+
# Set entire ceiling light to warm white
|
|
863
|
+
await ceiling.set_color(
|
|
864
|
+
HSBK(hue=0, saturation=0, brightness=1.0, kelvin=2700)
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
# Later component control will use this color
|
|
868
|
+
await ceiling.turn_uplight_off() # Uplight off
|
|
869
|
+
await ceiling.turn_uplight_on() # Restores to warm white
|
|
870
|
+
```
|
|
871
|
+
"""
|
|
872
|
+
# Call parent to perform actual color change
|
|
873
|
+
await super().set_color(color, duration)
|
|
874
|
+
|
|
875
|
+
# Update cached component colors - all zones now have the same color
|
|
876
|
+
self._last_uplight_color = color
|
|
877
|
+
self._last_downlight_colors = [color] * self.downlight_zone_count
|
|
878
|
+
|
|
879
|
+
# Also update stored state for restoration
|
|
880
|
+
self._stored_uplight_state = color
|
|
881
|
+
self._stored_downlight_state = [color] * self.downlight_zone_count
|
|
882
|
+
|
|
883
|
+
# Persist if enabled
|
|
884
|
+
if self._state_file:
|
|
885
|
+
self._save_state_to_file()
|
|
886
|
+
|
|
827
887
|
async def turn_downlight_off(
|
|
828
888
|
self, colors: HSBK | list[HSBK] | None = None, duration: float = 0.0
|
|
829
889
|
) -> None:
|
|
@@ -845,8 +905,6 @@ class CeilingLight(MatrixLight):
|
|
|
845
905
|
Note:
|
|
846
906
|
Sets all downlight zone brightness to 0 on device while preserving H, S, K.
|
|
847
907
|
"""
|
|
848
|
-
expected_count = len(range(*self.downlight_zones.indices(256)))
|
|
849
|
-
|
|
850
908
|
# Validate provided colors early (before fetching)
|
|
851
909
|
stored_colors: list[HSBK] | None = None
|
|
852
910
|
if colors is not None:
|
|
@@ -856,16 +914,16 @@ class CeilingLight(MatrixLight):
|
|
|
856
914
|
"Provided color cannot have brightness=0. "
|
|
857
915
|
"Omit the parameter to use current colors."
|
|
858
916
|
)
|
|
859
|
-
stored_colors = [colors] *
|
|
917
|
+
stored_colors = [colors] * self.downlight_zone_count
|
|
860
918
|
else:
|
|
861
919
|
if all(c.brightness == 0 for c in colors):
|
|
862
920
|
raise ValueError(
|
|
863
921
|
"Provided colors cannot have brightness=0. "
|
|
864
922
|
"Omit the parameter to use current colors."
|
|
865
923
|
)
|
|
866
|
-
if len(colors) !=
|
|
924
|
+
if len(colors) != self.downlight_zone_count:
|
|
867
925
|
raise ValueError(
|
|
868
|
-
f"Expected {
|
|
926
|
+
f"Expected {self.downlight_zone_count} colors for downlight, "
|
|
869
927
|
f"got {len(colors)}"
|
|
870
928
|
)
|
|
871
929
|
stored_colors = list(colors)
|
|
@@ -807,23 +807,49 @@ class TestCeilingLightBackwardCompatibility:
|
|
|
807
807
|
"""Create a Ceiling product 176 instance with mocked connection."""
|
|
808
808
|
ceiling = CeilingLight(serial="d073d5010203", ip="192.168.1.100")
|
|
809
809
|
ceiling.connection = AsyncMock()
|
|
810
|
+
ceiling._save_state_to_file = MagicMock()
|
|
811
|
+
|
|
812
|
+
# Mock version for product detection
|
|
813
|
+
ceiling._version = MagicMock()
|
|
814
|
+
ceiling._version.product = 176
|
|
810
815
|
return ceiling
|
|
811
816
|
|
|
812
817
|
async def test_set_color_affects_both_components(
|
|
813
818
|
self, ceiling_176: CeilingLight
|
|
814
819
|
) -> None:
|
|
815
|
-
"""Test
|
|
820
|
+
"""Test set_color calls parent and updates component state tracking."""
|
|
816
821
|
color = HSBK(hue=180, saturation=0.8, brightness=1.0, kelvin=5000)
|
|
817
822
|
|
|
818
|
-
# Mock the parent set_color method
|
|
819
|
-
ceiling_176.set_matrix_colors = AsyncMock()
|
|
820
|
-
|
|
821
823
|
await ceiling_176.set_color(color)
|
|
822
824
|
|
|
823
|
-
# Verify set_color was called (
|
|
824
|
-
# This would set all zones including both components
|
|
825
|
+
# Verify parent set_color was called (via connection.request)
|
|
825
826
|
assert ceiling_176.connection.request.called
|
|
826
827
|
|
|
828
|
+
# Verify uplight state was updated
|
|
829
|
+
assert ceiling_176._last_uplight_color == color
|
|
830
|
+
assert ceiling_176._stored_uplight_state == color
|
|
831
|
+
|
|
832
|
+
# Verify downlight state was updated (63 zones for product 176)
|
|
833
|
+
assert ceiling_176._last_downlight_colors == [color] * 63
|
|
834
|
+
assert ceiling_176._stored_downlight_state == [color] * 63
|
|
835
|
+
|
|
836
|
+
# Verify state was NOT persisted (no state_file configured)
|
|
837
|
+
ceiling_176._save_state_to_file.assert_not_called()
|
|
838
|
+
|
|
839
|
+
async def test_set_color_persists_state_when_file_configured(
|
|
840
|
+
self, ceiling_176: CeilingLight
|
|
841
|
+
) -> None:
|
|
842
|
+
"""Test set_color persists state when state_file is configured."""
|
|
843
|
+
color = HSBK(hue=180, saturation=0.8, brightness=1.0, kelvin=5000)
|
|
844
|
+
|
|
845
|
+
# Enable state file persistence
|
|
846
|
+
ceiling_176._state_file = "/tmp/test_state.json"
|
|
847
|
+
|
|
848
|
+
await ceiling_176.set_color(color)
|
|
849
|
+
|
|
850
|
+
# Verify state was persisted
|
|
851
|
+
ceiling_176._save_state_to_file.assert_called_once()
|
|
852
|
+
|
|
827
853
|
async def test_matrixlight_methods_still_work(
|
|
828
854
|
self, ceiling_176: CeilingLight
|
|
829
855
|
) -> None:
|
|
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
|
|
File without changes
|
|
File without changes
|