lifx-async 4.7.2__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.2 → lifx_async-4.7.4}/PKG-INFO +1 -1
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/changelog.md +16 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/pyproject.toml +1 -1
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/devices/ceiling.py +340 -97
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_ceiling.py +603 -15
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_effects/test_integration.py +56 -9
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_network/test_connection.py +4 -2
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_theme/test_canvas.py +4 -2
- {lifx_async-4.7.2 → lifx_async-4.7.4}/uv.lock +1 -1
- {lifx_async-4.7.2 → lifx_async-4.7.4}/.claude/settings.json +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/.github/dependabot.yml +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/.github/labeler.yml +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/.github/workflows/ci.yml +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/.github/workflows/docs.yml +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/.gitignore +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/.pre-commit-config.yaml +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/CLAUDE.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/LICENSE +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/README.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/context7.json +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/api/colors.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/api/devices.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/api/effects.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/api/exceptions.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/api/high-level.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/api/index.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/api/network.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/api/protocol.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/api/themes.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/architecture/overview.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/faq.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/getting-started/effects.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/getting-started/installation.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/getting-started/themes.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/index.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/stylesheets/extra.css +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/user-guide/advanced-usage.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/user-guide/ceiling-lights.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/user-guide/themes.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/examples/01_simple_discovery.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/examples/02_simple_control.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/examples/03_waveforms.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/examples/04_logging.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/examples/06_pulse_effect.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/examples/08_custom_effect.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/examples/09_background_effect.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/examples/11_matrix_basic.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/examples/12_matrix_effects.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/examples/13_matrix_large.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/mkdocs.yml +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/renovate.json +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/api.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/color.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/const.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/devices/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/devices/base.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/devices/hev.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/devices/infrared.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/devices/light.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/devices/matrix.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/devices/multizone.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/effects/base.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/effects/colorloop.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/effects/const.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/effects/models.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/exceptions.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/network/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/network/connection.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/network/discovery.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/network/message.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/network/transport.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/products/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/products/generator.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/products/quirks.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/products/registry.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/protocol/base.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/protocol/generator.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/protocol/header.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/protocol/models.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/protocol/protocol_types.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/py.typed +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/theme/generators.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/theme/library.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/src/lifx/theme/theme.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/conftest.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_api/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_api/test_api_apply_theme.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_api/test_api_batch_operations.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_api/test_api_discovery.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_color.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/conftest.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_base.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_light.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_matrix.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_multizone.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_state_ceiling.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_state_hev.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_state_infrared.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_state_light.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_state_management.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_state_matrix.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_devices/test_state_multizone.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_effects/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_effects/test_base.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_effects/test_models.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_network/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_network/test_concurrent_requests.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_network/test_message.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_network/test_transport.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_products/test_registry.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_protocol/test_serializer.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_theme/__init__.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_theme/conftest.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_theme/test_library.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-4.7.2 → lifx_async-4.7.4}/tests/test_utils.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
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
|
+
|
|
13
|
+
## v4.7.3 (2025-12-16)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **devices**: Capture component colors before set_power turns off light
|
|
18
|
+
([`a99abee`](https://github.com/Djelibeybi/lifx-async/commit/a99abeeeb4f6cad1e49410204b8e7a567765b3ed))
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
## v4.7.2 (2025-12-16)
|
|
6
22
|
|
|
7
23
|
### 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,145 @@ 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()
|
|
749
|
+
else:
|
|
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)
|
|
761
|
+
|
|
762
|
+
async def set_power(self, level: bool | int, duration: float = 0.0) -> None:
|
|
763
|
+
"""Set light power state, capturing component colors before turning off.
|
|
764
|
+
|
|
765
|
+
Overrides Light.set_power() to capture the current uplight and downlight
|
|
766
|
+
colors before turning off the entire light. This allows subsequent calls
|
|
767
|
+
to turn_uplight_on() or turn_downlight_on() to restore the colors that
|
|
768
|
+
were active just before the light was turned off.
|
|
769
|
+
|
|
770
|
+
The captured colors preserve hue, saturation, and kelvin values even if
|
|
771
|
+
a component was already off (brightness=0). The brightness will be
|
|
772
|
+
determined at turn-on time using the standard brightness inference logic.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
level: True/65535 to turn on, False/0 to turn off
|
|
776
|
+
duration: Transition duration in seconds (default 0.0)
|
|
777
|
+
|
|
778
|
+
Raises:
|
|
779
|
+
ValueError: If integer value is not 0 or 65535
|
|
780
|
+
LifxDeviceNotFoundError: If device is not connected
|
|
781
|
+
LifxTimeoutError: If device does not respond
|
|
782
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
783
|
+
|
|
784
|
+
Example:
|
|
785
|
+
```python
|
|
786
|
+
# Turn off entire ceiling light (captures colors for later)
|
|
787
|
+
await ceiling.set_power(False)
|
|
788
|
+
|
|
789
|
+
# Later, turn on just the uplight with its previous color
|
|
790
|
+
await ceiling.turn_uplight_on()
|
|
791
|
+
|
|
792
|
+
# Or turn on just the downlight with its previous colors
|
|
793
|
+
await ceiling.turn_downlight_on()
|
|
794
|
+
```
|
|
795
|
+
"""
|
|
796
|
+
# Determine if we're turning off
|
|
797
|
+
if isinstance(level, bool):
|
|
798
|
+
turning_off = not level
|
|
799
|
+
elif isinstance(level, int):
|
|
800
|
+
if level not in (0, 65535):
|
|
801
|
+
raise ValueError(f"Power level must be 0 or 65535, got {level}")
|
|
802
|
+
turning_off = level == 0
|
|
617
803
|
else:
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
804
|
+
raise TypeError(f"Expected bool or int, got {type(level).__name__}")
|
|
805
|
+
|
|
806
|
+
# If turning off, capture current colors for both components with single fetch
|
|
807
|
+
if turning_off:
|
|
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]
|
|
811
|
+
|
|
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
|
|
819
|
+
|
|
820
|
+
# Call parent to perform actual power change
|
|
821
|
+
await super().set_power(level, duration)
|
|
822
|
+
|
|
823
|
+
# Persist AFTER device operation completes
|
|
824
|
+
if turning_off and self._state_file:
|
|
825
|
+
self._save_state_to_file()
|
|
621
826
|
|
|
622
827
|
async def turn_downlight_off(
|
|
623
828
|
self, colors: HSBK | list[HSBK] | None = None, duration: float = 0.0
|
|
@@ -642,15 +847,16 @@ class CeilingLight(MatrixLight):
|
|
|
642
847
|
"""
|
|
643
848
|
expected_count = len(range(*self.downlight_zones.indices(256)))
|
|
644
849
|
|
|
850
|
+
# Validate provided colors early (before fetching)
|
|
851
|
+
stored_colors: list[HSBK] | None = None
|
|
645
852
|
if colors is not None:
|
|
646
|
-
# Validate and normalize provided colors
|
|
647
853
|
if isinstance(colors, HSBK):
|
|
648
854
|
if colors.brightness == 0:
|
|
649
855
|
raise ValueError(
|
|
650
856
|
"Provided color cannot have brightness=0. "
|
|
651
857
|
"Omit the parameter to use current colors."
|
|
652
858
|
)
|
|
653
|
-
|
|
859
|
+
stored_colors = [colors] * expected_count
|
|
654
860
|
else:
|
|
655
861
|
if all(c.brightness == 0 for c in colors):
|
|
656
862
|
raise ValueError(
|
|
@@ -662,13 +868,19 @@ class CeilingLight(MatrixLight):
|
|
|
662
868
|
f"Expected {expected_count} colors for downlight, "
|
|
663
869
|
f"got {len(colors)}"
|
|
664
870
|
)
|
|
665
|
-
|
|
871
|
+
stored_colors = list(colors)
|
|
666
872
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
|
672
884
|
|
|
673
885
|
# Create colors with brightness=0 for device
|
|
674
886
|
off_colors = [
|
|
@@ -678,12 +890,10 @@ class CeilingLight(MatrixLight):
|
|
|
678
890
|
brightness=0.0,
|
|
679
891
|
kelvin=c.kelvin,
|
|
680
892
|
)
|
|
681
|
-
for c in
|
|
893
|
+
for c in stored_colors
|
|
682
894
|
]
|
|
683
895
|
|
|
684
|
-
#
|
|
685
|
-
all_colors = await self.get_all_tile_colors()
|
|
686
|
-
tile_colors = all_colors[0]
|
|
896
|
+
# Update downlight zones and send immediately
|
|
687
897
|
tile_colors[self.downlight_zones] = off_colors
|
|
688
898
|
await self.set_matrix_colors(0, tile_colors, duration=int(duration * 1000))
|
|
689
899
|
|
|
@@ -694,89 +904,122 @@ class CeilingLight(MatrixLight):
|
|
|
694
904
|
if self._state_file:
|
|
695
905
|
self._save_state_to_file()
|
|
696
906
|
|
|
697
|
-
async def _determine_uplight_brightness(
|
|
907
|
+
async def _determine_uplight_brightness(
|
|
908
|
+
self, tile_colors: list[HSBK] | None = None
|
|
909
|
+
) -> HSBK:
|
|
698
910
|
"""Determine uplight brightness using priority logic.
|
|
699
911
|
|
|
700
912
|
Priority order:
|
|
701
|
-
1. Stored state (if available)
|
|
702
|
-
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)
|
|
703
915
|
3. Hardcoded default (0.8)
|
|
704
916
|
|
|
917
|
+
Args:
|
|
918
|
+
tile_colors: Optional pre-fetched tile colors to avoid redundant fetch.
|
|
919
|
+
If None, will fetch from device.
|
|
920
|
+
|
|
705
921
|
Returns:
|
|
706
922
|
HSBK color for uplight
|
|
707
923
|
"""
|
|
708
|
-
# 1. Stored state
|
|
709
|
-
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
|
+
):
|
|
710
929
|
return self._stored_uplight_state
|
|
711
930
|
|
|
712
|
-
# Get current
|
|
713
|
-
|
|
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
|
|
714
945
|
|
|
715
946
|
# 2. Infer from downlight average brightness
|
|
716
|
-
|
|
717
|
-
downlight_colors
|
|
718
|
-
|
|
719
|
-
downlight_colors
|
|
720
|
-
)
|
|
947
|
+
avg_brightness = sum(c.brightness for c in downlight_colors) / len(
|
|
948
|
+
downlight_colors
|
|
949
|
+
)
|
|
721
950
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
except Exception: # nosec B110
|
|
732
|
-
# If inference fails, fall through to default
|
|
733
|
-
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
|
+
)
|
|
734
960
|
|
|
735
961
|
# 3. Hardcoded default (0.8)
|
|
736
962
|
return HSBK(
|
|
737
|
-
hue=
|
|
738
|
-
saturation=
|
|
963
|
+
hue=source_color.hue,
|
|
964
|
+
saturation=source_color.saturation,
|
|
739
965
|
brightness=0.8,
|
|
740
|
-
kelvin=
|
|
966
|
+
kelvin=source_color.kelvin,
|
|
741
967
|
)
|
|
742
968
|
|
|
743
|
-
async def _determine_downlight_brightness(
|
|
969
|
+
async def _determine_downlight_brightness(
|
|
970
|
+
self, tile_colors: list[HSBK] | None = None
|
|
971
|
+
) -> list[HSBK]:
|
|
744
972
|
"""Determine downlight brightness using priority logic.
|
|
745
973
|
|
|
746
974
|
Priority order:
|
|
747
|
-
1. Stored state (if available)
|
|
975
|
+
1. Stored state (if available AND any brightness > 0)
|
|
748
976
|
2. Infer from uplight brightness
|
|
749
977
|
3. Hardcoded default (0.8)
|
|
750
978
|
|
|
979
|
+
Args:
|
|
980
|
+
tile_colors: Optional pre-fetched tile colors to avoid redundant fetch.
|
|
981
|
+
If None, will fetch from device.
|
|
982
|
+
|
|
751
983
|
Returns:
|
|
752
984
|
List of HSBK colors for downlight zones
|
|
753
985
|
"""
|
|
754
|
-
# 1. Stored state
|
|
986
|
+
# 1. Stored state (only if any color has brightness > 0)
|
|
755
987
|
if self._stored_downlight_state is not None:
|
|
756
|
-
|
|
988
|
+
if any(c.brightness > 0 for c in self._stored_downlight_state):
|
|
989
|
+
return self._stored_downlight_state
|
|
757
990
|
|
|
758
|
-
# Get current
|
|
759
|
-
|
|
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]
|
|
760
995
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
uplight_color = await self.get_uplight_color()
|
|
996
|
+
current_downlight = list(tile_colors[self.downlight_zones])
|
|
997
|
+
uplight_color = tile_colors[self.uplight_zone]
|
|
764
998
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
+
]
|
|
780
1023
|
|
|
781
1024
|
# 3. Hardcoded default (0.8)
|
|
782
1025
|
return [
|
|
@@ -786,7 +1029,7 @@ class CeilingLight(MatrixLight):
|
|
|
786
1029
|
brightness=0.8,
|
|
787
1030
|
kelvin=c.kelvin,
|
|
788
1031
|
)
|
|
789
|
-
for c in
|
|
1032
|
+
for c in source_colors
|
|
790
1033
|
]
|
|
791
1034
|
|
|
792
1035
|
def _is_stored_state_valid(
|