lifx-async 4.4.0__tar.gz → 4.4.1__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.4.0 → lifx_async-4.4.1}/PKG-INFO +1 -1
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/changelog.md +8 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/examples/12_matrix_effects.py +16 -16
- {lifx_async-4.4.0 → lifx_async-4.4.1}/pyproject.toml +1 -1
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/devices/matrix.py +73 -21
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/theme/generators.py +8 -2
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_state_matrix.py +29 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_theme/test_generators.py +100 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/uv.lock +1 -1
- {lifx_async-4.4.0 → lifx_async-4.4.1}/.claude/settings.json +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/.github/dependabot.yml +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/.github/labeler.yml +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/.github/workflows/ci.yml +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/.github/workflows/docs.yml +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/.gitignore +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/.pre-commit-config.yaml +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/CLAUDE.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/LICENSE +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/README.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/api/colors.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/api/devices.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/api/effects.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/api/exceptions.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/api/high-level.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/api/index.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/api/network.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/api/protocol.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/api/themes.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/architecture/overview.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/faq.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/getting-started/effects.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/getting-started/installation.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/getting-started/themes.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/index.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/stylesheets/extra.css +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/user-guide/advanced-usage.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/user-guide/themes.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/examples/01_simple_discovery.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/examples/02_simple_control.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/examples/03_waveforms.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/examples/04_logging.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/examples/06_pulse_effect.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/examples/08_custom_effect.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/examples/09_background_effect.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/examples/11_matrix_basic.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/examples/13_matrix_large.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/mkdocs.yml +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/renovate.json +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/api.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/color.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/const.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/devices/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/devices/base.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/devices/hev.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/devices/infrared.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/devices/light.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/devices/multizone.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/effects/base.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/effects/colorloop.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/effects/const.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/effects/models.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/exceptions.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/network/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/network/connection.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/network/discovery.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/network/message.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/network/transport.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/products/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/products/generator.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/products/registry.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/protocol/base.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/protocol/generator.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/protocol/header.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/protocol/models.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/protocol/protocol_types.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/py.typed +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/theme/library.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/src/lifx/theme/theme.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/conftest.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_api/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_api/test_api_apply_theme.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_api/test_api_batch_operations.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_api/test_api_discovery.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_color.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/conftest.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_base.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_light.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_matrix.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_multizone.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_state_hev.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_state_infrared.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_state_light.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_state_management.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_devices/test_state_multizone.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_effects/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_effects/test_base.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_effects/test_models.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_network/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_network/test_concurrent_requests.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_network/test_connection.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_network/test_message.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_network/test_transport.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_products/test_registry.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_protocol/test_serializer.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_theme/__init__.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_theme/conftest.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_theme/test_canvas.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_theme/test_library.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-4.4.0 → lifx_async-4.4.1}/tests/test_utils.py +0 -0
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v4.4.1 (2025-12-03)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **theme**: Prevent color displacement in multi-tile matrix theme application
|
|
10
|
+
([`ca936ec`](https://github.com/Djelibeybi/lifx-async/commit/ca936ec8df84fc42803182ae9898d243e017c5a3))
|
|
11
|
+
|
|
12
|
+
|
|
5
13
|
## v4.4.0 (2025-11-29)
|
|
6
14
|
|
|
7
15
|
### Features
|
|
@@ -7,7 +7,7 @@ import argparse
|
|
|
7
7
|
import asyncio
|
|
8
8
|
|
|
9
9
|
from lifx import HSBK, Colors, MatrixLight
|
|
10
|
-
from lifx.protocol.protocol_types import
|
|
10
|
+
from lifx.protocol.protocol_types import FirmwareEffect, TileEffectSkyType
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
async def main(ip: str):
|
|
@@ -26,9 +26,9 @@ async def main(ip: str):
|
|
|
26
26
|
|
|
27
27
|
# Get current effect state
|
|
28
28
|
print("Getting current effect state...")
|
|
29
|
-
current_effect = await matrix.
|
|
29
|
+
current_effect = await matrix.get_effect()
|
|
30
30
|
print(f"Current effect: {current_effect.effect_type}")
|
|
31
|
-
if current_effect.effect_type !=
|
|
31
|
+
if current_effect.effect_type != FirmwareEffect.OFF:
|
|
32
32
|
print(f" Speed: {current_effect.speed}")
|
|
33
33
|
print(f" Duration: {current_effect.duration}s")
|
|
34
34
|
if current_effect.palette:
|
|
@@ -38,8 +38,8 @@ async def main(ip: str):
|
|
|
38
38
|
# Demonstrate MORPH effect
|
|
39
39
|
print("Starting MORPH effect...")
|
|
40
40
|
print(" (smooth color transitions across tiles)")
|
|
41
|
-
await matrix.
|
|
42
|
-
effect_type=
|
|
41
|
+
await matrix.set_effect(
|
|
42
|
+
effect_type=FirmwareEffect.MORPH,
|
|
43
43
|
speed=5,
|
|
44
44
|
palette=[Colors.RED, Colors.BLUE, Colors.GREEN, Colors.PURPLE],
|
|
45
45
|
)
|
|
@@ -49,8 +49,8 @@ async def main(ip: str):
|
|
|
49
49
|
# Demonstrate FLAME effect
|
|
50
50
|
print("\nStarting FLAME effect...")
|
|
51
51
|
print(" (flickering fire animation)")
|
|
52
|
-
await matrix.
|
|
53
|
-
effect_type=
|
|
52
|
+
await matrix.set_effect(
|
|
53
|
+
effect_type=FirmwareEffect.FLAME,
|
|
54
54
|
speed=3,
|
|
55
55
|
palette=[Colors.ORANGE, Colors.RED, Colors.YELLOW],
|
|
56
56
|
)
|
|
@@ -60,8 +60,8 @@ async def main(ip: str):
|
|
|
60
60
|
# Demonstrate SKY effect with SUNRISE
|
|
61
61
|
print("\nStarting SKY effect with SUNRISE...")
|
|
62
62
|
print(" (sunrise color progression)")
|
|
63
|
-
await matrix.
|
|
64
|
-
effect_type=
|
|
63
|
+
await matrix.set_effect(
|
|
64
|
+
effect_type=FirmwareEffect.SKY,
|
|
65
65
|
speed=10,
|
|
66
66
|
sky_type=TileEffectSkyType.SUNRISE,
|
|
67
67
|
)
|
|
@@ -71,8 +71,8 @@ async def main(ip: str):
|
|
|
71
71
|
# Demonstrate SKY effect with CLOUDS
|
|
72
72
|
print("\nStarting SKY effect with CLOUDS...")
|
|
73
73
|
print(" (moving cloud patterns)")
|
|
74
|
-
await matrix.
|
|
75
|
-
effect_type=
|
|
74
|
+
await matrix.set_effect(
|
|
75
|
+
effect_type=FirmwareEffect.SKY,
|
|
76
76
|
speed=5,
|
|
77
77
|
sky_type=TileEffectSkyType.CLOUDS,
|
|
78
78
|
cloud_saturation_min=50,
|
|
@@ -86,11 +86,11 @@ async def main(ip: str):
|
|
|
86
86
|
ocean_palette = [
|
|
87
87
|
Colors.CYAN,
|
|
88
88
|
Colors.BLUE,
|
|
89
|
-
HSBK(hue=210
|
|
89
|
+
HSBK(hue=210, saturation=1.0, brightness=0.4, kelvin=3500), # deep blue
|
|
90
90
|
HSBK(36408, 65535, 38550, 3500), # Ocean blue
|
|
91
91
|
]
|
|
92
|
-
await matrix.
|
|
93
|
-
effect_type=
|
|
92
|
+
await matrix.set_effect(
|
|
93
|
+
effect_type=FirmwareEffect.MORPH,
|
|
94
94
|
speed=3,
|
|
95
95
|
palette=ocean_palette,
|
|
96
96
|
)
|
|
@@ -99,11 +99,11 @@ async def main(ip: str):
|
|
|
99
99
|
|
|
100
100
|
# Stop effect and restore
|
|
101
101
|
print("\nStopping effect...")
|
|
102
|
-
await matrix.
|
|
102
|
+
await matrix.set_effect(effect_type=FirmwareEffect.OFF)
|
|
103
103
|
await asyncio.sleep(1)
|
|
104
104
|
|
|
105
105
|
# Verify effect stopped
|
|
106
|
-
final_effect = await matrix.
|
|
106
|
+
final_effect = await matrix.get_effect()
|
|
107
107
|
print(f"Final effect state: {final_effect.effect_type}")
|
|
108
108
|
|
|
109
109
|
if power == 0:
|
|
@@ -14,7 +14,6 @@ Terminology:
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
import asyncio
|
|
18
17
|
import logging
|
|
19
18
|
import time
|
|
20
19
|
from dataclasses import asdict, dataclass
|
|
@@ -544,6 +543,57 @@ class MatrixLight(Light):
|
|
|
544
543
|
|
|
545
544
|
return result
|
|
546
545
|
|
|
546
|
+
async def get_all_tile_colors(self) -> list[list[HSBK]]:
|
|
547
|
+
"""Get colors for all tiles in the chain.
|
|
548
|
+
|
|
549
|
+
Fetches colors from each tile in the device chain and returns them
|
|
550
|
+
as a list of color lists (one per tile). This is the matrix equivalent
|
|
551
|
+
of MultiZoneLight's get_all_color_zones().
|
|
552
|
+
|
|
553
|
+
Always fetches from device. Tiles are queried sequentially to avoid
|
|
554
|
+
overwhelming the device with concurrent requests.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
List of color lists, one per tile. Each inner list contains
|
|
558
|
+
the colors for that tile (typically 64 for 8x8 tiles).
|
|
559
|
+
|
|
560
|
+
Raises:
|
|
561
|
+
LifxDeviceNotFoundError: If device is not connected
|
|
562
|
+
LifxTimeoutError: If device does not respond
|
|
563
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
564
|
+
|
|
565
|
+
Example:
|
|
566
|
+
```python
|
|
567
|
+
# Get colors for all tiles
|
|
568
|
+
all_colors = await matrix.get_all_tile_colors()
|
|
569
|
+
print(f"Device has {len(all_colors)} tiles")
|
|
570
|
+
for i, tile_colors in enumerate(all_colors):
|
|
571
|
+
print(f"Tile {i}: {len(tile_colors)} colors")
|
|
572
|
+
|
|
573
|
+
# Flatten to single list if needed
|
|
574
|
+
flat_colors = [c for tile in all_colors for c in tile]
|
|
575
|
+
```
|
|
576
|
+
"""
|
|
577
|
+
# Get device chain (use cached if available)
|
|
578
|
+
if self._device_chain is None:
|
|
579
|
+
device_chain = await self.get_device_chain()
|
|
580
|
+
else:
|
|
581
|
+
device_chain = self._device_chain
|
|
582
|
+
|
|
583
|
+
# Fetch colors from each tile sequentially
|
|
584
|
+
all_colors: list[list[HSBK]] = []
|
|
585
|
+
for tile in device_chain:
|
|
586
|
+
tile_colors = await self.get64(tile_index=tile.tile_index)
|
|
587
|
+
all_colors.append(tile_colors)
|
|
588
|
+
|
|
589
|
+
# Update state if it exists (flatten for state storage)
|
|
590
|
+
if self._state is not None and hasattr(self._state, "tile_colors"):
|
|
591
|
+
flat_colors = [c for tile_colors in all_colors for c in tile_colors]
|
|
592
|
+
self._state.tile_colors = flat_colors
|
|
593
|
+
self._state.last_updated = time.time()
|
|
594
|
+
|
|
595
|
+
return all_colors
|
|
596
|
+
|
|
547
597
|
async def set64(
|
|
548
598
|
self,
|
|
549
599
|
tile_index: int,
|
|
@@ -993,8 +1043,13 @@ class MatrixLight(Light):
|
|
|
993
1043
|
canvas = Canvas()
|
|
994
1044
|
for tile in tiles:
|
|
995
1045
|
canvas.add_points_for_tile((int(tile.user_x), int(tile.user_y)), theme)
|
|
996
|
-
|
|
997
|
-
|
|
1046
|
+
|
|
1047
|
+
# Shuffle and blur ONCE after all points are added
|
|
1048
|
+
# (Previously these were inside the loop, causing earlier tiles' points
|
|
1049
|
+
# to be shuffled/blurred multiple times, displacing them from their
|
|
1050
|
+
# intended positions and losing theme color variety)
|
|
1051
|
+
canvas.shuffle_points()
|
|
1052
|
+
canvas.blur_by_distance()
|
|
998
1053
|
|
|
999
1054
|
# Create tile canvas and fill in gaps for smooth interpolation
|
|
1000
1055
|
tile_canvas = Canvas()
|
|
@@ -1068,7 +1123,7 @@ class MatrixLight(Light):
|
|
|
1068
1123
|
async def refresh_state(self) -> None:
|
|
1069
1124
|
"""Refresh matrix light state from hardware.
|
|
1070
1125
|
|
|
1071
|
-
Fetches color, tiles, tile colors, and effect.
|
|
1126
|
+
Fetches color, tiles, tile colors for all tiles, and effect.
|
|
1072
1127
|
|
|
1073
1128
|
Raises:
|
|
1074
1129
|
RuntimeError: If state has not been initialized
|
|
@@ -1077,15 +1132,12 @@ class MatrixLight(Light):
|
|
|
1077
1132
|
"""
|
|
1078
1133
|
await super().refresh_state()
|
|
1079
1134
|
|
|
1080
|
-
# Fetch all matrix light state
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
effect_task = tg.create_task(self.get_effect())
|
|
1084
|
-
|
|
1085
|
-
tile_colors = colors_task.result()
|
|
1086
|
-
effect = effect_task.result()
|
|
1135
|
+
# Fetch all matrix light state sequentially to avoid overwhelming device
|
|
1136
|
+
all_tile_colors = await self.get_all_tile_colors()
|
|
1137
|
+
effect = await self.get_effect()
|
|
1087
1138
|
|
|
1088
|
-
|
|
1139
|
+
# Flatten tile colors for state storage
|
|
1140
|
+
self._state.tile_colors = [c for tile in all_tile_colors for c in tile]
|
|
1089
1141
|
self._state.effect = effect.effect_type
|
|
1090
1142
|
|
|
1091
1143
|
async def _initialize_state(self) -> MatrixLightState:
|
|
@@ -1103,24 +1155,24 @@ class MatrixLight(Light):
|
|
|
1103
1155
|
"""
|
|
1104
1156
|
light_state = await super()._initialize_state()
|
|
1105
1157
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
tile_colors_task = tg.create_task(self.get64())
|
|
1109
|
-
effect_task = tg.create_task(self.get_effect())
|
|
1110
|
-
|
|
1111
|
-
chain = chain_task.result()
|
|
1158
|
+
# Fetch matrix-specific state sequentially to avoid overwhelming device
|
|
1159
|
+
chain = await self.get_device_chain()
|
|
1112
1160
|
tile_orientations = {
|
|
1113
1161
|
index: tile.nearest_orientation for index, tile in enumerate(chain)
|
|
1114
1162
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1163
|
+
# get_all_tile_colors uses cached chain from above
|
|
1164
|
+
all_tile_colors = await self.get_all_tile_colors()
|
|
1165
|
+
effect = await self.get_effect()
|
|
1166
|
+
|
|
1167
|
+
# Flatten tile colors for state storage
|
|
1168
|
+
flat_tile_colors = [c for tile in all_tile_colors for c in tile]
|
|
1117
1169
|
|
|
1118
1170
|
# Create state instance with matrix fields
|
|
1119
1171
|
self._state = MatrixLightState.from_light_state(
|
|
1120
1172
|
light_state,
|
|
1121
1173
|
chain=chain,
|
|
1122
1174
|
tile_orientations=tile_orientations,
|
|
1123
|
-
tile_colors=
|
|
1175
|
+
tile_colors=flat_tile_colors,
|
|
1124
1176
|
effect=effect.effect_type,
|
|
1125
1177
|
)
|
|
1126
1178
|
|
|
@@ -165,10 +165,16 @@ class MatrixGenerator:
|
|
|
165
165
|
shuffled_theme = theme.shuffled()
|
|
166
166
|
shuffled_theme.ensure_color()
|
|
167
167
|
|
|
168
|
+
# Add points for all tiles first
|
|
168
169
|
for (left_x, top_y), (width, height) in self.coords_and_sizes:
|
|
169
170
|
canvas.add_points_for_tile((left_x, top_y), shuffled_theme)
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
|
|
172
|
+
# Shuffle and blur ONCE after all points are added
|
|
173
|
+
# (Previously these were inside the loop, causing earlier tiles' points
|
|
174
|
+
# to be shuffled/blurred multiple times, displacing them from their
|
|
175
|
+
# intended positions and losing theme color variety)
|
|
176
|
+
canvas.shuffle_points()
|
|
177
|
+
canvas.blur_by_distance()
|
|
172
178
|
|
|
173
179
|
# Create tile canvas and fill gaps
|
|
174
180
|
tile_canvas = Canvas()
|
|
@@ -152,6 +152,35 @@ class TestMatrixLightStateManagement:
|
|
|
152
152
|
retrieved = await matrix_light.get64(tile_index=0)
|
|
153
153
|
assert len(retrieved) == 64
|
|
154
154
|
|
|
155
|
+
@pytest.mark.asyncio
|
|
156
|
+
async def test_get_all_tile_colors_fetches_all_tiles(
|
|
157
|
+
self, emulator_devices
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Test get_all_tile_colors() fetches colors from all tiles in chain."""
|
|
160
|
+
template: MatrixLight = emulator_devices[6] # d073d5000007
|
|
161
|
+
|
|
162
|
+
async with await MatrixLight.connect(
|
|
163
|
+
serial=template.serial, ip=template.ip, port=template.port
|
|
164
|
+
) as matrix_light:
|
|
165
|
+
assert isinstance(matrix_light, MatrixLight)
|
|
166
|
+
|
|
167
|
+
# Get colors for all tiles
|
|
168
|
+
all_colors = await matrix_light.get_all_tile_colors()
|
|
169
|
+
|
|
170
|
+
# Should return a list of color lists (one per tile)
|
|
171
|
+
assert isinstance(all_colors, list)
|
|
172
|
+
assert len(all_colors) == matrix_light.tile_count
|
|
173
|
+
|
|
174
|
+
# Each tile should have the correct number of colors
|
|
175
|
+
for tile_colors in all_colors:
|
|
176
|
+
assert isinstance(tile_colors, list)
|
|
177
|
+
assert len(tile_colors) == 64 # 8x8 tile
|
|
178
|
+
assert all(isinstance(c, HSBK) for c in tile_colors)
|
|
179
|
+
|
|
180
|
+
# State should be updated with flattened colors
|
|
181
|
+
expected_total = matrix_light.tile_count * 64
|
|
182
|
+
assert len(matrix_light.state.tile_colors) == expected_total
|
|
183
|
+
|
|
155
184
|
@pytest.mark.asyncio
|
|
156
185
|
async def test_get_effect_updates_state(self, emulator_devices) -> None:
|
|
157
186
|
"""Test get_effect() updates state when it exists."""
|
|
@@ -172,3 +172,103 @@ class TestMatrixGenerator:
|
|
|
172
172
|
|
|
173
173
|
assert len(tiles) == 1
|
|
174
174
|
assert len(tiles[0]) == 256
|
|
175
|
+
|
|
176
|
+
def test_multiple_tiles_use_all_theme_colors(self) -> None:
|
|
177
|
+
"""Test that multiple tiles use colors from the entire theme, not just the last.
|
|
178
|
+
|
|
179
|
+
Regression test for bug where shuffle_points() and blur_by_distance() were
|
|
180
|
+
called inside the tile loop, causing points from earlier tiles to be displaced
|
|
181
|
+
multiple times, resulting in only the last tile's colors being visible.
|
|
182
|
+
"""
|
|
183
|
+
import random
|
|
184
|
+
|
|
185
|
+
# Use fixed seed for reproducibility
|
|
186
|
+
random.seed(42)
|
|
187
|
+
|
|
188
|
+
# Create a 5-tile chain (like a real LIFX Tile setup)
|
|
189
|
+
coords_and_sizes = [
|
|
190
|
+
((0, 0), (8, 8)),
|
|
191
|
+
((8, 0), (8, 8)),
|
|
192
|
+
((16, 0), (8, 8)),
|
|
193
|
+
((24, 0), (8, 8)),
|
|
194
|
+
((32, 0), (8, 8)),
|
|
195
|
+
]
|
|
196
|
+
gen = MatrixGenerator(coords_and_sizes)
|
|
197
|
+
|
|
198
|
+
# Theme with distinctly different colors (high saturation, different hues)
|
|
199
|
+
theme = Theme(
|
|
200
|
+
[Colors.RED, Colors.GREEN, Colors.BLUE, Colors.CYAN, Colors.MAGENTA]
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
tiles = gen.get_theme_colors(theme)
|
|
204
|
+
|
|
205
|
+
# Collect unique hue values across all tiles (rounded for float precision)
|
|
206
|
+
all_hues: set[int] = set()
|
|
207
|
+
for tile in tiles:
|
|
208
|
+
for color in tile:
|
|
209
|
+
# Round hue to nearest 10 degrees to group similar hues
|
|
210
|
+
rounded_hue = round(color.hue / 10) * 10
|
|
211
|
+
all_hues.add(rounded_hue)
|
|
212
|
+
|
|
213
|
+
# With 5 distinct theme colors, we should see variety across the tiles
|
|
214
|
+
# If the bug exists, we'd see very few unique hues (colors would converge)
|
|
215
|
+
# The theme has hues at approximately: 0 (red), 120 (green), 240 (blue),
|
|
216
|
+
# 180 (cyan), 300 (magenta)
|
|
217
|
+
# With blending, we expect to see intermediate hues too
|
|
218
|
+
assert len(all_hues) >= 3, (
|
|
219
|
+
f"Expected at least 3 distinct hue ranges but got {len(all_hues)}: "
|
|
220
|
+
f"{sorted(all_hues)}. This suggests colors are converging to a "
|
|
221
|
+
"single color instead of using the full theme."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def test_all_theme_colors_represented_in_output(self) -> None:
|
|
225
|
+
"""Test that all theme colors are represented in the generated output.
|
|
226
|
+
|
|
227
|
+
Regression test for bug where shuffle_points() and blur_by_distance() were
|
|
228
|
+
called inside the tile loop, causing points from earlier tiles to be displaced
|
|
229
|
+
multiple times. This resulted in only some theme colors appearing in the output.
|
|
230
|
+
"""
|
|
231
|
+
import random
|
|
232
|
+
|
|
233
|
+
from lifx.color import HSBK
|
|
234
|
+
|
|
235
|
+
# Use fixed seed for reproducibility
|
|
236
|
+
random.seed(12345)
|
|
237
|
+
|
|
238
|
+
# Create tiles that are far apart spatially
|
|
239
|
+
coords_and_sizes = [
|
|
240
|
+
((0, 0), (8, 8)),
|
|
241
|
+
((50, 0), (8, 8)),
|
|
242
|
+
]
|
|
243
|
+
gen = MatrixGenerator(coords_and_sizes)
|
|
244
|
+
|
|
245
|
+
# Theme with two distinct, non-wrapping colors (Yellow=60, Cyan=180)
|
|
246
|
+
# These are 120 degrees apart, so we should see hues spanning that range
|
|
247
|
+
theme = Theme(
|
|
248
|
+
[
|
|
249
|
+
HSBK(hue=60, saturation=1.0, brightness=1.0, kelvin=3500), # Yellow
|
|
250
|
+
HSBK(hue=180, saturation=1.0, brightness=1.0, kelvin=3500), # Cyan
|
|
251
|
+
]
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
tiles = gen.get_theme_colors(theme)
|
|
255
|
+
|
|
256
|
+
# Collect all hues across all tiles
|
|
257
|
+
all_hues = [c.hue for tile in tiles for c in tile]
|
|
258
|
+
|
|
259
|
+
# With Yellow (60) and Cyan (180), we expect hues to span from ~60 to ~180
|
|
260
|
+
# If the bug exists, we'd only see hues near one of the colors
|
|
261
|
+
min_hue = min(all_hues)
|
|
262
|
+
max_hue = max(all_hues)
|
|
263
|
+
hue_spread = max_hue - min_hue
|
|
264
|
+
|
|
265
|
+
# The spread should be at least 80 degrees (2/3 the distance between colors)
|
|
266
|
+
# if both theme colors are being represented.
|
|
267
|
+
# With the bug, we only see ~66 degree spread (62-128), missing cyan.
|
|
268
|
+
# With the fix, we see spread > 100 degrees as colors approach 60 and 180.
|
|
269
|
+
assert hue_spread >= 80, (
|
|
270
|
+
f"Hue spread is only {hue_spread:.1f} degrees "
|
|
271
|
+
f"(range: {min_hue:.0f}-{max_hue:.0f}). Expected at least 80 degrees "
|
|
272
|
+
"when theme has Yellow(60) and Cyan(180). "
|
|
273
|
+
"This suggests only one theme color is being used."
|
|
274
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|