lifx-async 4.7.3__tar.gz → 4.7.4__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.3 → lifx_async-4.7.4}/PKG-INFO +1 -1
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/changelog.md +8 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/pyproject.toml +1 -1
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/devices/ceiling.py +290 -106
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_ceiling.py +400 -47
- {lifx_async-4.7.3 → lifx_async-4.7.4}/uv.lock +1 -1
- {lifx_async-4.7.3 → lifx_async-4.7.4}/.claude/settings.json +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/.github/dependabot.yml +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/.github/labeler.yml +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/.github/workflows/ci.yml +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/.github/workflows/docs.yml +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/.gitignore +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/.pre-commit-config.yaml +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/CLAUDE.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/LICENSE +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/README.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/context7.json +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/api/colors.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/api/devices.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/api/effects.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/api/exceptions.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/api/high-level.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/api/index.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/api/network.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/api/protocol.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/api/themes.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/architecture/overview.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/faq.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/getting-started/effects.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/getting-started/installation.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/getting-started/themes.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/index.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/stylesheets/extra.css +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/user-guide/advanced-usage.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/user-guide/ceiling-lights.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/user-guide/themes.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/examples/01_simple_discovery.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/examples/02_simple_control.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/examples/03_waveforms.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/examples/04_logging.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/examples/06_pulse_effect.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/examples/08_custom_effect.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/examples/09_background_effect.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/examples/11_matrix_basic.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/examples/12_matrix_effects.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/examples/13_matrix_large.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/mkdocs.yml +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/renovate.json +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/api.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/color.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/const.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/devices/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/devices/base.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/devices/hev.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/devices/infrared.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/devices/light.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/devices/matrix.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/devices/multizone.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/effects/base.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/effects/colorloop.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/effects/const.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/effects/models.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/exceptions.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/network/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/network/connection.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/network/discovery.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/network/message.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/network/transport.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/products/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/products/generator.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/products/quirks.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/products/registry.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/protocol/base.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/protocol/generator.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/protocol/header.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/protocol/models.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/protocol/protocol_types.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/py.typed +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/theme/generators.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/theme/library.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/src/lifx/theme/theme.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/conftest.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_api/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_api/test_api_apply_theme.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_api/test_api_batch_operations.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_api/test_api_discovery.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_color.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/conftest.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_base.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_light.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_matrix.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_multizone.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_state_ceiling.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_state_hev.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_state_infrared.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_state_light.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_state_management.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_state_matrix.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_devices/test_state_multizone.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_effects/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_effects/test_base.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_effects/test_models.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_network/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_network/test_concurrent_requests.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_network/test_connection.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_network/test_message.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_network/test_transport.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_products/test_registry.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_protocol/test_serializer.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_theme/__init__.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_theme/conftest.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_theme/test_canvas.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_theme/test_library.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-4.7.3 → lifx_async-4.7.4}/tests/test_utils.py +0 -0
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v4.7.4 (2025-12-16)
|
|
6
|
+
|
|
7
|
+
### Performance Improvements
|
|
8
|
+
|
|
9
|
+
- **devices**: Reduce get_all_tile_colors calls in CeilingLight
|
|
10
|
+
([`3936158`](https://github.com/Djelibeybi/lifx-async/commit/39361582856fcde57f30f052b8286f0bbb695f67))
|
|
11
|
+
|
|
12
|
+
|
|
5
13
|
## v4.7.3 (2025-12-16)
|
|
6
14
|
|
|
7
15
|
### Bug Fixes
|
|
@@ -201,9 +201,15 @@ class CeilingLight(MatrixLight):
|
|
|
201
201
|
"""
|
|
202
202
|
matrix_state = await super()._initialize_state()
|
|
203
203
|
|
|
204
|
-
#
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
# Extract ceiling component colors from already-fetched tile_colors
|
|
205
|
+
# (parent _initialize_state already called get_all_tile_colors)
|
|
206
|
+
tile_colors = matrix_state.tile_colors
|
|
207
|
+
uplight_color = tile_colors[self.uplight_zone]
|
|
208
|
+
downlight_colors = list(tile_colors[self.downlight_zones])
|
|
209
|
+
|
|
210
|
+
# Cache for is_on properties
|
|
211
|
+
self._last_uplight_color = uplight_color
|
|
212
|
+
self._last_downlight_colors = downlight_colors
|
|
207
213
|
|
|
208
214
|
# Create ceiling state from matrix state
|
|
209
215
|
ceiling_state = CeilingLightState.from_matrix_state(
|
|
@@ -231,9 +237,15 @@ class CeilingLight(MatrixLight):
|
|
|
231
237
|
"""
|
|
232
238
|
await super().refresh_state()
|
|
233
239
|
|
|
234
|
-
#
|
|
235
|
-
|
|
236
|
-
|
|
240
|
+
# Extract ceiling component colors from already-fetched tile_colors
|
|
241
|
+
# (parent refresh_state already called get_all_tile_colors)
|
|
242
|
+
tile_colors = self._state.tile_colors
|
|
243
|
+
uplight_color = tile_colors[self.uplight_zone]
|
|
244
|
+
downlight_colors = list(tile_colors[self.downlight_zones])
|
|
245
|
+
|
|
246
|
+
# Cache for is_on properties
|
|
247
|
+
self._last_uplight_color = uplight_color
|
|
248
|
+
self._last_downlight_colors = downlight_colors
|
|
237
249
|
|
|
238
250
|
# Update ceiling-specific state fields
|
|
239
251
|
state = cast(CeilingLightState, self._state)
|
|
@@ -522,6 +534,10 @@ class CeilingLight(MatrixLight):
|
|
|
522
534
|
) -> None:
|
|
523
535
|
"""Turn uplight component on.
|
|
524
536
|
|
|
537
|
+
If the entire light is off, this will set the color instantly and then
|
|
538
|
+
turn on the light with the specified duration, so the light fades to
|
|
539
|
+
the target color instead of flashing to its previous state.
|
|
540
|
+
|
|
525
541
|
Args:
|
|
526
542
|
color: Optional HSBK color. If provided:
|
|
527
543
|
- Uses this color immediately
|
|
@@ -533,14 +549,61 @@ class CeilingLight(MatrixLight):
|
|
|
533
549
|
ValueError: If color.brightness == 0
|
|
534
550
|
LifxTimeoutError: Device did not respond
|
|
535
551
|
"""
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
552
|
+
# Validate provided color early
|
|
553
|
+
if color is not None and color.brightness == 0:
|
|
554
|
+
raise ValueError("Cannot turn on uplight with brightness=0")
|
|
555
|
+
|
|
556
|
+
# Check if light is off first to determine which path to take
|
|
557
|
+
if await self.get_power() == 0:
|
|
558
|
+
# Light is off - single fetch for both determining color and modification
|
|
559
|
+
all_colors = await self.get_all_tile_colors()
|
|
560
|
+
tile_colors = all_colors[0]
|
|
561
|
+
|
|
562
|
+
# Determine target color (pass pre-fetched colors to avoid extra fetch)
|
|
563
|
+
if color is not None:
|
|
564
|
+
target_color = color
|
|
565
|
+
else:
|
|
566
|
+
target_color = await self._determine_uplight_brightness(tile_colors)
|
|
567
|
+
|
|
568
|
+
# Store current downlight colors BEFORE zeroing them out
|
|
569
|
+
# This allows turn_downlight_on() to restore them later
|
|
570
|
+
downlight_colors = tile_colors[self.downlight_zones]
|
|
571
|
+
self._stored_downlight_state = list(downlight_colors)
|
|
572
|
+
|
|
573
|
+
# Set uplight zone to target color
|
|
574
|
+
tile_colors[self.uplight_zone] = target_color
|
|
575
|
+
|
|
576
|
+
# Zero out downlight zones so they stay off when power turns on
|
|
577
|
+
for i in range(*self.downlight_zones.indices(len(tile_colors))):
|
|
578
|
+
tile_colors[i] = HSBK(
|
|
579
|
+
hue=tile_colors[i].hue,
|
|
580
|
+
saturation=tile_colors[i].saturation,
|
|
581
|
+
brightness=0.0,
|
|
582
|
+
kelvin=tile_colors[i].kelvin,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# Set all colors instantly (duration=0) while light is off
|
|
586
|
+
await self.set_matrix_colors(0, tile_colors, duration=0)
|
|
587
|
+
|
|
588
|
+
# Update stored state for uplight
|
|
589
|
+
self._stored_uplight_state = target_color
|
|
590
|
+
self._last_uplight_color = target_color
|
|
591
|
+
|
|
592
|
+
# Turn on with the requested duration - light fades on to target color
|
|
593
|
+
await super().set_power(True, duration)
|
|
594
|
+
|
|
595
|
+
# Persist AFTER device operations complete
|
|
596
|
+
if self._state_file:
|
|
597
|
+
self._save_state_to_file()
|
|
540
598
|
else:
|
|
541
|
-
#
|
|
542
|
-
|
|
543
|
-
|
|
599
|
+
# Light is already on - determine target color first, then set
|
|
600
|
+
if color is not None:
|
|
601
|
+
target_color = color
|
|
602
|
+
else:
|
|
603
|
+
target_color = await self._determine_uplight_brightness()
|
|
604
|
+
|
|
605
|
+
# set_uplight_color will fetch and modify (single fetch in that method)
|
|
606
|
+
await self.set_uplight_color(target_color, duration)
|
|
544
607
|
|
|
545
608
|
async def turn_uplight_off(
|
|
546
609
|
self, color: HSBK | None = None, duration: float = 0.0
|
|
@@ -560,30 +623,35 @@ class CeilingLight(MatrixLight):
|
|
|
560
623
|
Note:
|
|
561
624
|
Sets uplight zone brightness to 0 on device while preserving H, S, K.
|
|
562
625
|
"""
|
|
626
|
+
if color is not None and color.brightness == 0:
|
|
627
|
+
raise ValueError(
|
|
628
|
+
"Provided color cannot have brightness=0. "
|
|
629
|
+
"Omit the parameter to use current color."
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
# Fetch current state once and reuse to calculate brightness
|
|
633
|
+
all_colors = await self.get_all_tile_colors()
|
|
634
|
+
tile_colors = all_colors[0]
|
|
635
|
+
|
|
636
|
+
# Determine which color to store
|
|
563
637
|
if color is not None:
|
|
564
|
-
|
|
565
|
-
raise ValueError(
|
|
566
|
-
"Provided color cannot have brightness=0. "
|
|
567
|
-
"Omit the parameter to use current color."
|
|
568
|
-
)
|
|
569
|
-
# Store the provided color
|
|
570
|
-
self._stored_uplight_state = color
|
|
638
|
+
stored_color = color
|
|
571
639
|
else:
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
640
|
+
stored_color = tile_colors[self.uplight_zone]
|
|
641
|
+
self._last_uplight_color = stored_color
|
|
642
|
+
|
|
643
|
+
# Store for future restoration
|
|
644
|
+
self._stored_uplight_state = stored_color
|
|
575
645
|
|
|
576
646
|
# Create color with brightness=0 for device
|
|
577
647
|
off_color = HSBK(
|
|
578
|
-
hue=
|
|
579
|
-
saturation=
|
|
648
|
+
hue=stored_color.hue,
|
|
649
|
+
saturation=stored_color.saturation,
|
|
580
650
|
brightness=0.0,
|
|
581
|
-
kelvin=
|
|
651
|
+
kelvin=stored_color.kelvin,
|
|
582
652
|
)
|
|
583
653
|
|
|
584
|
-
#
|
|
585
|
-
all_colors = await self.get_all_tile_colors()
|
|
586
|
-
tile_colors = all_colors[0]
|
|
654
|
+
# Update uplight zone and send immediately
|
|
587
655
|
tile_colors[self.uplight_zone] = off_color
|
|
588
656
|
await self.set_matrix_colors(0, tile_colors, duration=int(duration * 1000))
|
|
589
657
|
|
|
@@ -599,6 +667,10 @@ class CeilingLight(MatrixLight):
|
|
|
599
667
|
) -> None:
|
|
600
668
|
"""Turn downlight component on.
|
|
601
669
|
|
|
670
|
+
If the entire light is off, this will set the colors instantly and then
|
|
671
|
+
turn on the light with the specified duration, so the light fades to
|
|
672
|
+
the target colors instead of flashing to its previous state.
|
|
673
|
+
|
|
602
674
|
Args:
|
|
603
675
|
colors: Optional colors. Can be:
|
|
604
676
|
- None: uses brightness determination logic
|
|
@@ -612,12 +684,80 @@ class CeilingLight(MatrixLight):
|
|
|
612
684
|
ValueError: If list length doesn't match downlight zone count
|
|
613
685
|
LifxTimeoutError: Device did not respond
|
|
614
686
|
"""
|
|
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
|
+
# Validate provided colors early
|
|
615
692
|
if colors is not None:
|
|
616
|
-
|
|
693
|
+
if isinstance(colors, HSBK):
|
|
694
|
+
if colors.brightness == 0:
|
|
695
|
+
raise ValueError("Cannot turn on downlight with brightness=0")
|
|
696
|
+
else:
|
|
697
|
+
if all(c.brightness == 0 for c in colors):
|
|
698
|
+
raise ValueError("Cannot turn on downlight with brightness=0")
|
|
699
|
+
if len(colors) != downlight_zone_count:
|
|
700
|
+
raise ValueError(
|
|
701
|
+
f"Expected {downlight_zone_count} colors for downlight, "
|
|
702
|
+
f"got {len(colors)}"
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
# Check if light is off first to determine which path to take
|
|
706
|
+
if await self.get_power() == 0:
|
|
707
|
+
# Light is off - single fetch for both determining colors and modification
|
|
708
|
+
all_colors = await self.get_all_tile_colors()
|
|
709
|
+
tile_colors = all_colors[0]
|
|
710
|
+
|
|
711
|
+
# Determine target colors (pass pre-fetched colors to avoid extra fetch)
|
|
712
|
+
if colors is not None:
|
|
713
|
+
if isinstance(colors, HSBK):
|
|
714
|
+
target_colors = [colors] * downlight_zone_count
|
|
715
|
+
else:
|
|
716
|
+
target_colors = list(colors)
|
|
717
|
+
else:
|
|
718
|
+
target_colors = await self._determine_downlight_brightness(tile_colors)
|
|
719
|
+
|
|
720
|
+
# Store current uplight color BEFORE zeroing it out
|
|
721
|
+
# This allows turn_uplight_on() to restore it later
|
|
722
|
+
self._stored_uplight_state = tile_colors[self.uplight_zone]
|
|
723
|
+
|
|
724
|
+
# Set downlight zones to target colors
|
|
725
|
+
tile_colors[self.downlight_zones] = target_colors
|
|
726
|
+
|
|
727
|
+
# Zero out uplight zone so it stays off when power turns on
|
|
728
|
+
uplight_color = tile_colors[self.uplight_zone]
|
|
729
|
+
tile_colors[self.uplight_zone] = HSBK(
|
|
730
|
+
hue=uplight_color.hue,
|
|
731
|
+
saturation=uplight_color.saturation,
|
|
732
|
+
brightness=0.0,
|
|
733
|
+
kelvin=uplight_color.kelvin,
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
# Set all colors instantly (duration=0) while light is off
|
|
737
|
+
await self.set_matrix_colors(0, tile_colors, duration=0)
|
|
738
|
+
|
|
739
|
+
# Update stored state for downlight
|
|
740
|
+
self._stored_downlight_state = target_colors
|
|
741
|
+
self._last_downlight_colors = target_colors
|
|
742
|
+
|
|
743
|
+
# Turn on with the requested duration - light fades on to target colors
|
|
744
|
+
await super().set_power(True, duration)
|
|
745
|
+
|
|
746
|
+
# Persist AFTER device operations complete
|
|
747
|
+
if self._state_file:
|
|
748
|
+
self._save_state_to_file()
|
|
617
749
|
else:
|
|
618
|
-
#
|
|
619
|
-
|
|
620
|
-
|
|
750
|
+
# Light is already on - determine target colors first, then set
|
|
751
|
+
if colors is not None:
|
|
752
|
+
if isinstance(colors, HSBK):
|
|
753
|
+
target_colors = [colors] * downlight_zone_count
|
|
754
|
+
else:
|
|
755
|
+
target_colors = list(colors)
|
|
756
|
+
else:
|
|
757
|
+
target_colors = await self._determine_downlight_brightness()
|
|
758
|
+
|
|
759
|
+
# set_downlight_colors will fetch and modify (single fetch in that method)
|
|
760
|
+
await self.set_downlight_colors(target_colors, duration)
|
|
621
761
|
|
|
622
762
|
async def set_power(self, level: bool | int, duration: float = 0.0) -> None:
|
|
623
763
|
"""Set light power state, capturing component colors before turning off.
|
|
@@ -663,21 +803,27 @@ class CeilingLight(MatrixLight):
|
|
|
663
803
|
else:
|
|
664
804
|
raise TypeError(f"Expected bool or int, got {type(level).__name__}")
|
|
665
805
|
|
|
666
|
-
# If turning off, capture current colors for both components
|
|
806
|
+
# If turning off, capture current colors for both components with single fetch
|
|
667
807
|
if turning_off:
|
|
668
|
-
#
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
self._stored_uplight_state = await self.get_uplight_color()
|
|
672
|
-
self._stored_downlight_state = await self.get_downlight_colors()
|
|
808
|
+
# Single fetch to capture both uplight and downlight colors
|
|
809
|
+
all_colors = await self.get_all_tile_colors()
|
|
810
|
+
tile_colors = all_colors[0]
|
|
673
811
|
|
|
674
|
-
#
|
|
675
|
-
|
|
676
|
-
|
|
812
|
+
# Extract and store both component colors
|
|
813
|
+
self._stored_uplight_state = tile_colors[self.uplight_zone]
|
|
814
|
+
self._stored_downlight_state = list(tile_colors[self.downlight_zones])
|
|
815
|
+
|
|
816
|
+
# Also update cache for is_on properties
|
|
817
|
+
self._last_uplight_color = self._stored_uplight_state
|
|
818
|
+
self._last_downlight_colors = self._stored_downlight_state
|
|
677
819
|
|
|
678
820
|
# Call parent to perform actual power change
|
|
679
821
|
await super().set_power(level, duration)
|
|
680
822
|
|
|
823
|
+
# Persist AFTER device operation completes
|
|
824
|
+
if turning_off and self._state_file:
|
|
825
|
+
self._save_state_to_file()
|
|
826
|
+
|
|
681
827
|
async def turn_downlight_off(
|
|
682
828
|
self, colors: HSBK | list[HSBK] | None = None, duration: float = 0.0
|
|
683
829
|
) -> None:
|
|
@@ -701,15 +847,16 @@ class CeilingLight(MatrixLight):
|
|
|
701
847
|
"""
|
|
702
848
|
expected_count = len(range(*self.downlight_zones.indices(256)))
|
|
703
849
|
|
|
850
|
+
# Validate provided colors early (before fetching)
|
|
851
|
+
stored_colors: list[HSBK] | None = None
|
|
704
852
|
if colors is not None:
|
|
705
|
-
# Validate and normalize provided colors
|
|
706
853
|
if isinstance(colors, HSBK):
|
|
707
854
|
if colors.brightness == 0:
|
|
708
855
|
raise ValueError(
|
|
709
856
|
"Provided color cannot have brightness=0. "
|
|
710
857
|
"Omit the parameter to use current colors."
|
|
711
858
|
)
|
|
712
|
-
|
|
859
|
+
stored_colors = [colors] * expected_count
|
|
713
860
|
else:
|
|
714
861
|
if all(c.brightness == 0 for c in colors):
|
|
715
862
|
raise ValueError(
|
|
@@ -721,13 +868,19 @@ class CeilingLight(MatrixLight):
|
|
|
721
868
|
f"Expected {expected_count} colors for downlight, "
|
|
722
869
|
f"got {len(colors)}"
|
|
723
870
|
)
|
|
724
|
-
|
|
871
|
+
stored_colors = list(colors)
|
|
725
872
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
873
|
+
# Fetch current state once and reuse to calculate brightness
|
|
874
|
+
all_colors = await self.get_all_tile_colors()
|
|
875
|
+
tile_colors = all_colors[0]
|
|
876
|
+
|
|
877
|
+
# If colors not provided, extract from fetched data
|
|
878
|
+
if stored_colors is None:
|
|
879
|
+
stored_colors = list(tile_colors[self.downlight_zones])
|
|
880
|
+
self._last_downlight_colors = stored_colors
|
|
881
|
+
|
|
882
|
+
# Store for future restoration
|
|
883
|
+
self._stored_downlight_state = stored_colors
|
|
731
884
|
|
|
732
885
|
# Create colors with brightness=0 for device
|
|
733
886
|
off_colors = [
|
|
@@ -737,12 +890,10 @@ class CeilingLight(MatrixLight):
|
|
|
737
890
|
brightness=0.0,
|
|
738
891
|
kelvin=c.kelvin,
|
|
739
892
|
)
|
|
740
|
-
for c in
|
|
893
|
+
for c in stored_colors
|
|
741
894
|
]
|
|
742
895
|
|
|
743
|
-
#
|
|
744
|
-
all_colors = await self.get_all_tile_colors()
|
|
745
|
-
tile_colors = all_colors[0]
|
|
896
|
+
# Update downlight zones and send immediately
|
|
746
897
|
tile_colors[self.downlight_zones] = off_colors
|
|
747
898
|
await self.set_matrix_colors(0, tile_colors, duration=int(duration * 1000))
|
|
748
899
|
|
|
@@ -753,89 +904,122 @@ class CeilingLight(MatrixLight):
|
|
|
753
904
|
if self._state_file:
|
|
754
905
|
self._save_state_to_file()
|
|
755
906
|
|
|
756
|
-
async def _determine_uplight_brightness(
|
|
907
|
+
async def _determine_uplight_brightness(
|
|
908
|
+
self, tile_colors: list[HSBK] | None = None
|
|
909
|
+
) -> HSBK:
|
|
757
910
|
"""Determine uplight brightness using priority logic.
|
|
758
911
|
|
|
759
912
|
Priority order:
|
|
760
|
-
1. Stored state (if available)
|
|
761
|
-
2. Infer from downlight average brightness
|
|
913
|
+
1. Stored state (if available AND brightness > 0)
|
|
914
|
+
2. Infer from downlight average brightness (using stored H, S, K if available)
|
|
762
915
|
3. Hardcoded default (0.8)
|
|
763
916
|
|
|
917
|
+
Args:
|
|
918
|
+
tile_colors: Optional pre-fetched tile colors to avoid redundant fetch.
|
|
919
|
+
If None, will fetch from device.
|
|
920
|
+
|
|
764
921
|
Returns:
|
|
765
922
|
HSBK color for uplight
|
|
766
923
|
"""
|
|
767
|
-
# 1. Stored state
|
|
768
|
-
if
|
|
924
|
+
# 1. Stored state (only if brightness > 0)
|
|
925
|
+
if (
|
|
926
|
+
self._stored_uplight_state is not None
|
|
927
|
+
and self._stored_uplight_state.brightness > 0
|
|
928
|
+
):
|
|
769
929
|
return self._stored_uplight_state
|
|
770
930
|
|
|
771
|
-
# Get current
|
|
772
|
-
|
|
931
|
+
# Get current colors (use pre-fetched if available)
|
|
932
|
+
if tile_colors is None:
|
|
933
|
+
all_colors = await self.get_all_tile_colors()
|
|
934
|
+
tile_colors = all_colors[0]
|
|
935
|
+
|
|
936
|
+
current_uplight = tile_colors[self.uplight_zone]
|
|
937
|
+
downlight_colors = tile_colors[self.downlight_zones]
|
|
938
|
+
|
|
939
|
+
# Cache for is_on properties
|
|
940
|
+
self._last_uplight_color = current_uplight
|
|
941
|
+
self._last_downlight_colors = list(downlight_colors)
|
|
942
|
+
|
|
943
|
+
# Determine which color source to use for H, S, K
|
|
944
|
+
source_color = self._stored_uplight_state or current_uplight
|
|
773
945
|
|
|
774
946
|
# 2. Infer from downlight average brightness
|
|
775
|
-
|
|
776
|
-
downlight_colors
|
|
777
|
-
|
|
778
|
-
downlight_colors
|
|
779
|
-
)
|
|
947
|
+
avg_brightness = sum(c.brightness for c in downlight_colors) / len(
|
|
948
|
+
downlight_colors
|
|
949
|
+
)
|
|
780
950
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
except Exception: # nosec B110
|
|
791
|
-
# If inference fails, fall through to default
|
|
792
|
-
pass
|
|
951
|
+
# Only use inferred brightness if it's > 0
|
|
952
|
+
# If all downlights are off (brightness=0), skip to default
|
|
953
|
+
if avg_brightness > 0:
|
|
954
|
+
return HSBK(
|
|
955
|
+
hue=source_color.hue,
|
|
956
|
+
saturation=source_color.saturation,
|
|
957
|
+
brightness=avg_brightness,
|
|
958
|
+
kelvin=source_color.kelvin,
|
|
959
|
+
)
|
|
793
960
|
|
|
794
961
|
# 3. Hardcoded default (0.8)
|
|
795
962
|
return HSBK(
|
|
796
|
-
hue=
|
|
797
|
-
saturation=
|
|
963
|
+
hue=source_color.hue,
|
|
964
|
+
saturation=source_color.saturation,
|
|
798
965
|
brightness=0.8,
|
|
799
|
-
kelvin=
|
|
966
|
+
kelvin=source_color.kelvin,
|
|
800
967
|
)
|
|
801
968
|
|
|
802
|
-
async def _determine_downlight_brightness(
|
|
969
|
+
async def _determine_downlight_brightness(
|
|
970
|
+
self, tile_colors: list[HSBK] | None = None
|
|
971
|
+
) -> list[HSBK]:
|
|
803
972
|
"""Determine downlight brightness using priority logic.
|
|
804
973
|
|
|
805
974
|
Priority order:
|
|
806
|
-
1. Stored state (if available)
|
|
975
|
+
1. Stored state (if available AND any brightness > 0)
|
|
807
976
|
2. Infer from uplight brightness
|
|
808
977
|
3. Hardcoded default (0.8)
|
|
809
978
|
|
|
979
|
+
Args:
|
|
980
|
+
tile_colors: Optional pre-fetched tile colors to avoid redundant fetch.
|
|
981
|
+
If None, will fetch from device.
|
|
982
|
+
|
|
810
983
|
Returns:
|
|
811
984
|
List of HSBK colors for downlight zones
|
|
812
985
|
"""
|
|
813
|
-
# 1. Stored state
|
|
986
|
+
# 1. Stored state (only if any color has brightness > 0)
|
|
814
987
|
if self._stored_downlight_state is not None:
|
|
815
|
-
|
|
988
|
+
if any(c.brightness > 0 for c in self._stored_downlight_state):
|
|
989
|
+
return self._stored_downlight_state
|
|
816
990
|
|
|
817
|
-
# Get current
|
|
818
|
-
|
|
991
|
+
# Get current colors (use pre-fetched if available)
|
|
992
|
+
if tile_colors is None:
|
|
993
|
+
all_colors = await self.get_all_tile_colors()
|
|
994
|
+
tile_colors = all_colors[0]
|
|
819
995
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
uplight_color = await self.get_uplight_color()
|
|
996
|
+
current_downlight = list(tile_colors[self.downlight_zones])
|
|
997
|
+
uplight_color = tile_colors[self.uplight_zone]
|
|
823
998
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
999
|
+
# Cache for is_on properties
|
|
1000
|
+
self._last_downlight_colors = current_downlight
|
|
1001
|
+
self._last_uplight_color = uplight_color
|
|
1002
|
+
|
|
1003
|
+
# Prefer stored H, S, K if available, otherwise use current
|
|
1004
|
+
source_colors: list[HSBK] = (
|
|
1005
|
+
self._stored_downlight_state
|
|
1006
|
+
if self._stored_downlight_state is not None
|
|
1007
|
+
else current_downlight
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
# 2. Infer from uplight brightness
|
|
1011
|
+
# Only use inferred brightness if it's > 0
|
|
1012
|
+
# If uplight is off (brightness=0), skip to default
|
|
1013
|
+
if uplight_color.brightness > 0:
|
|
1014
|
+
return [
|
|
1015
|
+
HSBK(
|
|
1016
|
+
hue=c.hue,
|
|
1017
|
+
saturation=c.saturation,
|
|
1018
|
+
brightness=uplight_color.brightness,
|
|
1019
|
+
kelvin=c.kelvin,
|
|
1020
|
+
)
|
|
1021
|
+
for c in source_colors
|
|
1022
|
+
]
|
|
839
1023
|
|
|
840
1024
|
# 3. Hardcoded default (0.8)
|
|
841
1025
|
return [
|
|
@@ -845,7 +1029,7 @@ class CeilingLight(MatrixLight):
|
|
|
845
1029
|
brightness=0.8,
|
|
846
1030
|
kelvin=c.kelvin,
|
|
847
1031
|
)
|
|
848
|
-
for c in
|
|
1032
|
+
for c in source_colors
|
|
849
1033
|
]
|
|
850
1034
|
|
|
851
1035
|
def _is_stored_state_valid(
|