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