lifx-async 4.3.1__tar.gz → 4.3.3__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 (144) hide show
  1. {lifx_async-4.3.1 → lifx_async-4.3.3}/.github/workflows/pr-automation.yml +2 -0
  2. {lifx_async-4.3.1 → lifx_async-4.3.3}/PKG-INFO +1 -1
  3. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/changelog.md +19 -0
  4. {lifx_async-4.3.1 → lifx_async-4.3.3}/examples/11_matrix_basic.py +1 -3
  5. {lifx_async-4.3.1 → lifx_async-4.3.3}/pyproject.toml +1 -1
  6. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/devices/matrix.py +75 -31
  7. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/effects/base.py +10 -0
  8. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/effects/colorloop.py +9 -0
  9. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/effects/pulse.py +9 -0
  10. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_devices/test_matrix.py +70 -109
  11. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_effects/test_base.py +5 -0
  12. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_effects/test_colorloop.py +1 -0
  13. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_effects/test_integration.py +5 -0
  14. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_effects/test_models.py +5 -0
  15. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_effects/test_pulse.py +1 -0
  16. {lifx_async-4.3.1 → lifx_async-4.3.3}/uv.lock +1 -1
  17. {lifx_async-4.3.1 → lifx_async-4.3.3}/.claude/settings.json +0 -0
  18. {lifx_async-4.3.1 → lifx_async-4.3.3}/.github/dependabot.yml +0 -0
  19. {lifx_async-4.3.1 → lifx_async-4.3.3}/.github/labeler.yml +0 -0
  20. {lifx_async-4.3.1 → lifx_async-4.3.3}/.github/workflows/ci.yml +0 -0
  21. {lifx_async-4.3.1 → lifx_async-4.3.3}/.github/workflows/docs.yml +0 -0
  22. {lifx_async-4.3.1 → lifx_async-4.3.3}/.gitignore +0 -0
  23. {lifx_async-4.3.1 → lifx_async-4.3.3}/.pre-commit-config.yaml +0 -0
  24. {lifx_async-4.3.1 → lifx_async-4.3.3}/CLAUDE.md +0 -0
  25. {lifx_async-4.3.1 → lifx_async-4.3.3}/LICENSE +0 -0
  26. {lifx_async-4.3.1 → lifx_async-4.3.3}/README.md +0 -0
  27. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/api/colors.md +0 -0
  28. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/api/devices.md +0 -0
  29. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/api/effects.md +0 -0
  30. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/api/exceptions.md +0 -0
  31. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/api/high-level.md +0 -0
  32. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/api/index.md +0 -0
  33. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/api/network.md +0 -0
  34. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/api/protocol.md +0 -0
  35. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/api/themes.md +0 -0
  36. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/architecture/effects-architecture.md +0 -0
  37. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/architecture/overview.md +0 -0
  38. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/faq.md +0 -0
  39. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/getting-started/effects.md +0 -0
  40. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/getting-started/installation.md +0 -0
  41. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/getting-started/quickstart.md +0 -0
  42. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/getting-started/themes.md +0 -0
  43. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/index.md +0 -0
  44. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/migration/effect-api-changes.md +0 -0
  45. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/stylesheets/extra.css +0 -0
  46. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/user-guide/advanced-usage.md +0 -0
  47. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/user-guide/effects-custom.md +0 -0
  48. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/user-guide/effects-troubleshooting.md +0 -0
  49. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/user-guide/protocol-deep-dive.md +0 -0
  50. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/user-guide/themes.md +0 -0
  51. {lifx_async-4.3.1 → lifx_async-4.3.3}/docs/user-guide/troubleshooting.md +0 -0
  52. {lifx_async-4.3.1 → lifx_async-4.3.3}/examples/01_simple_discovery.py +0 -0
  53. {lifx_async-4.3.1 → lifx_async-4.3.3}/examples/02_simple_control.py +0 -0
  54. {lifx_async-4.3.1 → lifx_async-4.3.3}/examples/03_waveforms.py +0 -0
  55. {lifx_async-4.3.1 → lifx_async-4.3.3}/examples/04_logging.py +0 -0
  56. {lifx_async-4.3.1 → lifx_async-4.3.3}/examples/06_pulse_effect.py +0 -0
  57. {lifx_async-4.3.1 → lifx_async-4.3.3}/examples/07_colorloop_effect.py +0 -0
  58. {lifx_async-4.3.1 → lifx_async-4.3.3}/examples/08_custom_effect.py +0 -0
  59. {lifx_async-4.3.1 → lifx_async-4.3.3}/examples/09_background_effect.py +0 -0
  60. {lifx_async-4.3.1 → lifx_async-4.3.3}/examples/10_find_specific_devices.py +0 -0
  61. {lifx_async-4.3.1 → lifx_async-4.3.3}/examples/12_matrix_effects.py +0 -0
  62. {lifx_async-4.3.1 → lifx_async-4.3.3}/examples/13_matrix_large.py +0 -0
  63. {lifx_async-4.3.1 → lifx_async-4.3.3}/mkdocs.yml +0 -0
  64. {lifx_async-4.3.1 → lifx_async-4.3.3}/renovate.json +0 -0
  65. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/__init__.py +0 -0
  66. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/api.py +0 -0
  67. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/color.py +0 -0
  68. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/const.py +0 -0
  69. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/devices/__init__.py +0 -0
  70. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/devices/base.py +0 -0
  71. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/devices/hev.py +0 -0
  72. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/devices/infrared.py +0 -0
  73. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/devices/light.py +0 -0
  74. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/devices/multizone.py +0 -0
  75. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/effects/__init__.py +0 -0
  76. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/effects/conductor.py +0 -0
  77. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/effects/const.py +0 -0
  78. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/effects/models.py +0 -0
  79. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/effects/state_manager.py +0 -0
  80. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/exceptions.py +0 -0
  81. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/network/__init__.py +0 -0
  82. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/network/connection.py +0 -0
  83. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/network/discovery.py +0 -0
  84. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/network/message.py +0 -0
  85. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/network/transport.py +0 -0
  86. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/products/__init__.py +0 -0
  87. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/products/generator.py +0 -0
  88. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/products/registry.py +0 -0
  89. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/protocol/__init__.py +0 -0
  90. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/protocol/base.py +0 -0
  91. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/protocol/generator.py +0 -0
  92. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/protocol/header.py +0 -0
  93. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/protocol/models.py +0 -0
  94. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/protocol/packets.py +0 -0
  95. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/protocol/protocol_types.py +0 -0
  96. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/protocol/serializer.py +0 -0
  97. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/py.typed +0 -0
  98. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/theme/__init__.py +0 -0
  99. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/theme/canvas.py +0 -0
  100. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/theme/generators.py +0 -0
  101. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/theme/library.py +0 -0
  102. {lifx_async-4.3.1 → lifx_async-4.3.3}/src/lifx/theme/theme.py +0 -0
  103. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/__init__.py +0 -0
  104. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/conftest.py +0 -0
  105. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_api/__init__.py +0 -0
  106. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_api/test_api_apply_theme.py +0 -0
  107. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_api/test_api_batch_errors.py +0 -0
  108. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_api/test_api_batch_operations.py +0 -0
  109. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_api/test_api_discovery.py +0 -0
  110. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_api/test_api_organization.py +0 -0
  111. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_color.py +0 -0
  112. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_devices/__init__.py +0 -0
  113. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_devices/conftest.py +0 -0
  114. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_devices/test_base.py +0 -0
  115. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_devices/test_hev.py +0 -0
  116. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_devices/test_infrared.py +0 -0
  117. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_devices/test_light.py +0 -0
  118. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_devices/test_mac_address.py +0 -0
  119. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_devices/test_multizone.py +0 -0
  120. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_effects/__init__.py +0 -0
  121. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_effects/test_capability_filtering.py +0 -0
  122. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_effects/test_state_manager.py +0 -0
  123. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_network/__init__.py +0 -0
  124. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_network/test_concurrent_requests.py +0 -0
  125. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_network/test_connection.py +0 -0
  126. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_network/test_discovery_devices.py +0 -0
  127. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_network/test_discovery_errors.py +0 -0
  128. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_network/test_message.py +0 -0
  129. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_network/test_message_advanced.py +0 -0
  130. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_network/test_transport.py +0 -0
  131. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_products/test_product_generator.py +0 -0
  132. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_products/test_registry.py +0 -0
  133. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_protocol/test_generated.py +0 -0
  134. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_protocol/test_header.py +0 -0
  135. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_protocol/test_protocol_generator.py +0 -0
  136. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_protocol/test_serializer.py +0 -0
  137. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_theme/__init__.py +0 -0
  138. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_theme/conftest.py +0 -0
  139. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_theme/test_apply_theme.py +0 -0
  140. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_theme/test_canvas.py +0 -0
  141. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_theme/test_generators.py +0 -0
  142. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_theme/test_library.py +0 -0
  143. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_theme/test_theme.py +0 -0
  144. {lifx_async-4.3.1 → lifx_async-4.3.3}/tests/test_utils.py +0 -0
@@ -71,6 +71,8 @@ jobs:
71
71
  docs
72
72
  ci
73
73
  deps
74
+ effects
75
+ themes
74
76
  requireScope: false
75
77
  subjectPattern: ^(?![A-Z]).+$
76
78
  subjectPatternError: |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-async
3
- Version: 4.3.1
3
+ Version: 4.3.3
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,25 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v4.3.3 (2025-11-22)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Give MatrixLight.get64() some default parameters
10
+ ([`a69a49c`](https://github.com/Djelibeybi/lifx-async/commit/a69a49c93488c79c8c3be58a9304fd01b4b12231))
11
+
12
+ - **themes**: Apply theme colors to all zones via proper canvas interpolation
13
+ ([`f1628c4`](https://github.com/Djelibeybi/lifx-async/commit/f1628c4a071d257d7db79a7945d1516c783d8d52))
14
+
15
+
16
+ ## v4.3.2 (2025-11-22)
17
+
18
+ ### Bug Fixes
19
+
20
+ - **effects**: Add name property to LIFXEffect and subclasses
21
+ ([`deb8a54`](https://github.com/Djelibeybi/lifx-async/commit/deb8a54f674d2d4cd9b8dce519dc6ca8678e048a))
22
+
23
+
5
24
  ## v4.3.1 (2025-11-22)
6
25
 
7
26
  ### Bug Fixes
@@ -35,9 +35,7 @@ async def main(ip: str):
35
35
 
36
36
  # Get current colors from first tile
37
37
  print("Getting current colors from tile 0...")
38
- tile_colors = await matrix.get64(
39
- tile_index=0, length=1, x=0, y=0, width=device_chain[0].width
40
- )
38
+ tile_colors = await matrix.get64()
41
39
  print(f"Retrieved {len(tile_colors)} colors\n")
42
40
 
43
41
  if power == 0:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lifx-async"
3
- version = "4.3.1"
3
+ version = "4.3.3"
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"
@@ -371,45 +371,58 @@ class MatrixLight(Light):
371
371
 
372
372
  async def get64(
373
373
  self,
374
- tile_index: int,
375
- length: int,
376
- x: int,
377
- y: int,
378
- width: int,
379
- fb_index: int = 0,
374
+ tile_index: int = 0,
375
+ length: int = 1,
376
+ x: int = 0,
377
+ y: int = 0,
378
+ width: int | None = None,
380
379
  ) -> list[HSBK]:
381
380
  """Get up to 64 zones of color state from a tile.
382
381
 
382
+ For devices with ≤64 zones, returns all zones. For devices with >64 zones,
383
+ returns up to 64 zones due to protocol limitations.
384
+
383
385
  Args:
384
- tile_index: Index of the tile (0-based)
385
- length: Number of tiles to query (usually 1)
386
- x: X coordinate of the rectangle (0-based)
387
- y: Y coordinate of the rectangle (0-based)
388
- width: Width of the rectangle in zones
389
- fb_index: Frame buffer index (0 for display, 1 for temp buffer)
386
+ tile_index: Index of the tile (0-based). Defaults to 0.
387
+ length: Number of tiles to query (usually 1). Defaults to 1.
388
+ x: X coordinate of the rectangle (0-based). Defaults to 0.
389
+ y: Y coordinate of the rectangle (0-based). Defaults to 0.
390
+ width: Width of the rectangle in zones. Defaults to tile width.
390
391
 
391
392
  Returns:
392
- List of HSBK colors for the requested zones
393
+ List of HSBK colors for the requested zones. For tiles with ≤64 zones,
394
+ returns the actual zone count (e.g., 64 for 8x8, 16 for 4x4). For tiles
395
+ with >64 zones (e.g., 128 for 16x8 Ceiling), returns 64 (protocol limit).
393
396
 
394
397
  Example:
395
- >>> # Get colors from 8x8 tile (64 zones)
396
- >>> colors = await matrix.get64(tile_index=0, length=1, x=0, y=0, width=8)
398
+ >>> # Get all colors from first tile (no parameters needed)
399
+ >>> colors = await matrix.get64()
400
+ >>>
401
+ >>> # Get colors from specific region
402
+ >>> colors = await matrix.get64(y=4) # Start at row 4
397
403
  """
398
404
  # Validate parameters
399
405
  if x < 0:
400
406
  raise ValueError(f"x coordinate must be non-negative, got {x}")
401
407
  if y < 0:
402
408
  raise ValueError(f"y coordinate must be non-negative, got {y}")
403
- if width <= 0:
409
+ if width is not None and width <= 0:
404
410
  raise ValueError(f"width must be positive, got {width}")
405
411
 
412
+ if self._device_chain is None:
413
+ device_chain = await self.get_device_chain()
414
+ else:
415
+ device_chain = self._device_chain
416
+
417
+ if width is None:
418
+ width = device_chain[0].width
419
+
406
420
  _LOGGER.debug(
407
- "Getting 64 zones from tile %d (x=%d, y=%d, width=%d, fb=%d) for %s",
421
+ "Getting 64 zones from tile %d (x=%d, y=%d, width=%d) for %s",
408
422
  tile_index,
409
423
  x,
410
424
  y,
411
425
  width,
412
- fb_index,
413
426
  self.label or self.serial,
414
427
  )
415
428
 
@@ -417,12 +430,17 @@ class MatrixLight(Light):
417
430
  packets.Tile.Get64(
418
431
  tile_index=tile_index,
419
432
  length=length,
420
- rect=TileBufferRect(fb_index=fb_index, x=x, y=y, width=width),
433
+ rect=TileBufferRect(fb_index=0, x=x, y=y, width=width),
421
434
  )
422
435
  )
423
436
 
437
+ max_colors = device_chain[0].width * device_chain[0].height
438
+
424
439
  # Convert protocol colors to HSBK
425
- return [HSBK.from_protocol(proto_color) for proto_color in response.colors]
440
+ return [
441
+ HSBK.from_protocol(proto_color)
442
+ for proto_color in response.colors[:max_colors]
443
+ ]
426
444
 
427
445
  async def set64(
428
446
  self,
@@ -504,7 +522,11 @@ class MatrixLight(Light):
504
522
  )
505
523
 
506
524
  async def copy_frame_buffer(
507
- self, tile_index: int, source_fb: int = 1, target_fb: int = 0
525
+ self,
526
+ tile_index: int,
527
+ source_fb: int = 1,
528
+ target_fb: int = 0,
529
+ duration: float = 0.0,
508
530
  ) -> None:
509
531
  """Copy frame buffer (for tiles with >64 zones).
510
532
 
@@ -515,6 +537,7 @@ class MatrixLight(Light):
515
537
  tile_index: Index of the tile (0-based)
516
538
  source_fb: Source frame buffer index (usually 1)
517
539
  target_fb: Target frame buffer index (usually 0)
540
+ duration: time in seconds to transition if target_fb is 0
518
541
 
519
542
  Example:
520
543
  >>> # For 16x8 tile (128 zones):
@@ -541,7 +564,9 @@ class MatrixLight(Light):
541
564
  ... fb_index=1,
542
565
  ... )
543
566
  >>> # 3. Copy buffer 1 to buffer 0 (display)
544
- >>> await matrix.copy_frame_buffer(tile_index=0, source_fb=1, target_fb=0)
567
+ >>> await matrix.copy_frame_buffer(
568
+ ... tile_index=0, source_fb=1, target_fb=0, duration=2.0
569
+ ... )
545
570
  """
546
571
  _LOGGER.debug(
547
572
  "Copying frame buffer %d -> %d for tile %d on %s",
@@ -559,6 +584,7 @@ class MatrixLight(Light):
559
584
  raise ValueError(f"Invalid tile_index {tile_index}")
560
585
 
561
586
  tile = self._device_chain[tile_index]
587
+ duration_ms = round(duration * 1000 if duration else 0)
562
588
 
563
589
  await self.connection.send_packet(
564
590
  packets.Tile.CopyFrameBuffer(
@@ -572,7 +598,7 @@ class MatrixLight(Light):
572
598
  dst_y=0,
573
599
  width=tile.width,
574
600
  height=tile.height,
575
- duration=0,
601
+ duration=duration_ms,
576
602
  )
577
603
  )
578
604
 
@@ -723,7 +749,7 @@ class MatrixLight(Light):
723
749
  async def set_effect(
724
750
  self,
725
751
  effect_type: FirmwareEffect,
726
- speed: int = 3000,
752
+ speed: float = 3.0,
727
753
  duration: int = 0,
728
754
  palette: list[HSBK] | None = None,
729
755
  sky_type: TileEffectSkyType = TileEffectSkyType.SUNRISE,
@@ -734,7 +760,7 @@ class MatrixLight(Light):
734
760
 
735
761
  Args:
736
762
  effect_type: Type of effect (OFF, MORPH, FLAME, SKY)
737
- speed: Effect speed in milliseconds (default: 3000)
763
+ speed: Effect speed in seconds (default: 3)
738
764
  duration: Total effect duration in nanoseconds (0 for infinite)
739
765
  palette: Color palette for the effect (max 16 colors)
740
766
  sky_type: Sky effect type (SUNRISE, SUNSET, CLOUDS)
@@ -751,7 +777,7 @@ class MatrixLight(Light):
751
777
  ... ]
752
778
  >>> await matrix.set_effect(
753
779
  ... effect_type=FirmwareEffect.MORPH,
754
- ... speed=5000,
780
+ ... speed=5.0,
755
781
  ... palette=rainbow,
756
782
  ... )
757
783
  """
@@ -761,11 +787,12 @@ class MatrixLight(Light):
761
787
  speed,
762
788
  self.label or self.serial,
763
789
  )
790
+ speed_ms = round(speed * 1000) if speed else 3000
764
791
 
765
792
  # Create and validate MatrixEffect
766
793
  effect = MatrixEffect(
767
794
  effect_type=effect_type,
768
- speed=speed,
795
+ speed=speed_ms,
769
796
  duration=duration,
770
797
  palette=palette,
771
798
  sky_type=sky_type,
@@ -843,9 +870,23 @@ class MatrixLight(Light):
843
870
  # Create canvas and populate with theme colors
844
871
  canvas = Canvas()
845
872
  for tile in tiles:
846
- canvas.add_points_for_tile(None, theme)
847
- canvas.shuffle_points()
848
- canvas.blur_by_distance()
873
+ canvas.add_points_for_tile((int(tile.user_x), int(tile.user_y)), theme)
874
+ canvas.shuffle_points()
875
+ canvas.blur_by_distance()
876
+
877
+ # Create tile canvas and fill in gaps for smooth interpolation
878
+ tile_canvas = Canvas()
879
+ for tile in tiles:
880
+ tile_canvas.fill_in_points(
881
+ canvas,
882
+ int(tile.user_x),
883
+ int(tile.user_y),
884
+ tile.width,
885
+ tile.height,
886
+ )
887
+
888
+ # Final blur for smooth gradients
889
+ tile_canvas.blur()
849
890
 
850
891
  # Check if light is on
851
892
  is_on = await self.get_power()
@@ -853,7 +894,10 @@ class MatrixLight(Light):
853
894
  # Apply colors to each tile
854
895
  for tile in tiles:
855
896
  # Extract tile colors from canvas as 1D list
856
- colors = canvas.points_for_tile(None, width=tile.width, height=tile.height)
897
+ tile_coords = (int(tile.user_x), int(tile.user_y))
898
+ colors = tile_canvas.points_for_tile(
899
+ tile_coords, width=tile.width, height=tile.height
900
+ )
857
901
 
858
902
  # Apply with appropriate timing
859
903
  if power_on and not is_on:
@@ -59,6 +59,16 @@ class LIFXEffect(ABC):
59
59
  self.conductor: Conductor | None = None
60
60
  self.participants: list[Light] = []
61
61
 
62
+ @property
63
+ @abstractmethod
64
+ def name(self) -> str:
65
+ """Return the name of the effect.
66
+
67
+ Returns:
68
+ The effect name as a string
69
+ """
70
+ raise NotImplementedError("Subclasses must implement name property")
71
+
62
72
  async def async_perform(self, participants: list[Light]) -> None:
63
73
  """Perform common setup and play the effect.
64
74
 
@@ -127,6 +127,15 @@ class EffectColorloop(LIFXEffect):
127
127
  self._running = False
128
128
  self._stop_event = asyncio.Event()
129
129
 
130
+ @property
131
+ def name(self) -> str:
132
+ """Return the name of the effect.
133
+
134
+ Returns:
135
+ The effect name 'colorloop'
136
+ """
137
+ return "colorloop"
138
+
130
139
  async def async_play(self) -> None:
131
140
  """Execute the colorloop effect continuously."""
132
141
  self._running = True
@@ -136,6 +136,15 @@ class EffectPulse(LIFXEffect):
136
136
  if self.cycles < 1:
137
137
  raise ValueError(f"Cycles must be 1 or higher, got {self.cycles}")
138
138
 
139
+ @property
140
+ def name(self) -> str:
141
+ """Return the name of the effect.
142
+
143
+ Returns:
144
+ The effect name 'pulse'
145
+ """
146
+ return "pulse"
147
+
139
148
  async def async_play(self) -> None:
140
149
  """Execute the pulse effect on all participants."""
141
150
  # Determine colors for each light
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import pytest
6
6
 
7
- from lifx.color import HSBK
7
+ from lifx.color import HSBK, Colors
8
8
  from lifx.devices.matrix import MatrixEffect, MatrixLight, TileInfo
9
9
  from lifx.protocol.protocol_types import FirmwareEffect, TileEffectSkyType
10
10
 
@@ -74,23 +74,22 @@ class TestMatrixLight:
74
74
  assert matrix.tile_count == len(await matrix.get_device_chain())
75
75
 
76
76
  async def test_get64_single_tile(self, emulator_devices) -> None:
77
- """Test getting colors from 8x8 tile (64 zones)."""
77
+ """Test getting colors from 8x8 tile (64 zones) with default parameters."""
78
78
  matrix = emulator_devices[6]
79
79
  async with matrix:
80
80
  chain = await matrix.get_device_chain()
81
81
  tile = chain[0]
82
82
 
83
- # Get colors from first tile
84
- colors = await matrix.get64(
85
- tile_index=0,
86
- length=1,
87
- x=0,
88
- y=0,
89
- width=tile.width,
90
- )
83
+ # Verify tile dimensions
84
+ assert tile.width == 8
85
+ assert tile.height == 8
86
+ assert tile.total_zones == 64
87
+
88
+ # Get colors using default parameters (no args needed)
89
+ colors = await matrix.get64()
91
90
 
92
91
  assert isinstance(colors, list)
93
- assert len(colors) > 0
92
+ assert len(colors) == 64 # Returns actual number of zones
94
93
  assert all(isinstance(color, HSBK) for color in colors)
95
94
 
96
95
  async def test_set64_single_tile(self, emulator_devices) -> None:
@@ -102,7 +101,7 @@ class TestMatrixLight:
102
101
 
103
102
  # Set all zones to red
104
103
  zone_count = tile.width * tile.height
105
- red_colors = [HSBK.from_rgb(255, 0, 0)] * min(zone_count, 64)
104
+ red_colors = [Colors.RED] * min(zone_count, 64)
106
105
 
107
106
  await matrix.set64(
108
107
  tile_index=0,
@@ -115,13 +114,7 @@ class TestMatrixLight:
115
114
  )
116
115
 
117
116
  # Verify colors were set (read back)
118
- colors = await matrix.get64(
119
- tile_index=0,
120
- length=1,
121
- x=0,
122
- y=0,
123
- width=tile.width,
124
- )
117
+ colors = await matrix.get64()
125
118
 
126
119
  # First color should be red (allow protocol conversion tolerance)
127
120
  assert colors[0].hue < 10 or colors[0].hue > 350 # Red ~0 deg
@@ -144,13 +137,7 @@ class TestMatrixLight:
144
137
  await matrix.set_matrix_colors(tile_index=0, colors=gradient, duration=0)
145
138
 
146
139
  # Verify first few colors (partial verification to avoid test complexity)
147
- colors = await matrix.get64(
148
- tile_index=0,
149
- length=1,
150
- x=0,
151
- y=0,
152
- width=tile.width,
153
- )
140
+ colors = await matrix.get64()
154
141
  assert len(colors) > 0
155
142
 
156
143
  async def test_set_matrix_colors_solid_color(self, emulator_devices) -> None:
@@ -167,24 +154,18 @@ class TestMatrixLight:
167
154
  tile = chain[0]
168
155
 
169
156
  # Create solid red across all zones
170
- red_colors = [HSBK.from_rgb(255, 0, 0)] * tile.total_zones
157
+ red_colors = [Colors.RED] * tile.total_zones
171
158
 
172
159
  # Set all zones to red (should use SetColor packet)
173
160
  await matrix.set_matrix_colors(tile_index=0, colors=red_colors, duration=0)
174
161
 
175
162
  # Verify the color was set by reading back
176
- colors = await matrix.get64(
177
- tile_index=0,
178
- length=1,
179
- x=0,
180
- y=0,
181
- width=tile.width,
182
- )
163
+ colors = await matrix.get64()
183
164
 
184
165
  # Verify first zone is red
185
- assert colors[0].hue < 10 or colors[0].hue > 350
186
- assert colors[0].saturation > 0.9
187
- assert colors[0].brightness > 0.9
166
+ assert colors[0].hue == 0
167
+ assert colors[0].saturation == 1.0
168
+ assert colors[0].brightness == 1.0
188
169
 
189
170
  async def test_get_effect(self, emulator_devices) -> None:
190
171
  """Test getting current tile effect."""
@@ -217,10 +198,10 @@ class TestMatrixLight:
217
198
  async with matrix:
218
199
  # Create rainbow palette
219
200
  rainbow = [
220
- HSBK(0, 1.0, 1.0, 3500), # Red
221
- HSBK(60, 1.0, 1.0, 3500), # Yellow
222
- HSBK(120, 1.0, 1.0, 3500), # Green
223
- HSBK(240, 1.0, 1.0, 3500), # Blue
201
+ Colors.RED, # Red
202
+ Colors.YELLOW, # Yellow
203
+ Colors.GREEN, # Green
204
+ Colors.BLUE, # Blue
224
205
  ]
225
206
 
226
207
  await matrix.set_effect(
@@ -240,10 +221,10 @@ class TestMatrixLight:
240
221
  async with matrix:
241
222
  # Flame effect with fire colors
242
223
  fire_palette = [
243
- HSBK.from_rgb(255, 0, 0), # Red
244
- HSBK.from_rgb(255, 69, 0), # Orange-red
245
- HSBK.from_rgb(255, 140, 0), # Orange
246
- HSBK.from_rgb(255, 215, 0), # Gold
224
+ Colors.RED, # Red
225
+ HSBK(hue=16, saturation=1.0, brightness=1.0, kelvin=3500),
226
+ Colors.ORANGE, # Orange
227
+ HSBK(hue=51, saturation=1.0, brightness=1.0, kelvin=3500),
247
228
  ]
248
229
 
249
230
  await matrix.set_effect(
@@ -341,11 +322,11 @@ class TestMatrixLight:
341
322
  assert effect.effect_type == FirmwareEffect.OFF
342
323
 
343
324
  async def test_get64_large_tile(self, ceiling_device) -> None:
344
- """Test getting colors from 16x8 tile (128 zones).
325
+ """Test getting colors from 16x8 tile (128 zones) with default parameters.
345
326
 
346
- Ceiling devices have 16x8 tiles with 128 zones, which is larger than
347
- the 64-zone limit for direct get64 operations. This tests the protocol
348
- can handle reading from tiles with >64 zones.
327
+ Ceiling devices have 16x8 tiles with 128 zones. The get64() method returns
328
+ up to 64 colors due to protocol limitations, so we have to send two get64
329
+ requests.
349
330
  """
350
331
  matrix = ceiling_device
351
332
  async with matrix:
@@ -357,17 +338,13 @@ class TestMatrixLight:
357
338
  assert tile.height == 8
358
339
  assert tile.total_zones == 128
359
340
 
360
- # Get first 64 zones from the large tile
361
- colors = await matrix.get64(
362
- tile_index=0,
363
- length=1,
364
- x=0,
365
- y=0,
366
- width=tile.width,
367
- )
341
+ # Get zones using two get64 requests, 64 zones per request.
342
+ colors: list[HSBK] = []
343
+ colors.extend(await matrix.get64())
344
+ colors.extend(await matrix.get64(y=4))
368
345
 
369
346
  assert isinstance(colors, list)
370
- assert len(colors) == 64 # get64 returns max 64 colors
347
+ assert len(colors) == 128
371
348
  assert all(isinstance(color, HSBK) for color in colors)
372
349
 
373
350
  async def test_set64_large_tile(self, ceiling_device) -> None:
@@ -385,9 +362,12 @@ class TestMatrixLight:
385
362
  assert tile.total_zones == 128
386
363
 
387
364
  # Create 64 blue colors for first 64 zones
388
- blue_colors = [HSBK.from_rgb(0, 0, 255)] * 64
365
+ blue_colors = [Colors.BLUE] * 64
366
+
367
+ # Create 64 red colors for the second 64 zones
368
+ red_colors = [Colors.RED] * 64
389
369
 
390
- # Set first 64 zones to blue (writes to frame buffer 1)
370
+ # Set first 64 zones to blue (on frame buffer 1)
391
371
  await matrix.set64(
392
372
  tile_index=0,
393
373
  length=1,
@@ -399,32 +379,39 @@ class TestMatrixLight:
399
379
  fb_index=1, # Write to temp buffer
400
380
  )
401
381
 
402
- # Copy frame buffer 1 to frame buffer 0 (display)
403
- await matrix.copy_frame_buffer(tile_index=0, source_fb=1, target_fb=0)
404
-
405
- # Read back the first 64 zones from display buffer
406
- colors = await matrix.get64(
382
+ # Set the second 64 zones to red (on frame buffer 1)
383
+ await matrix.set64(
407
384
  tile_index=0,
408
385
  length=1,
409
386
  x=0,
410
- y=0,
387
+ y=4,
411
388
  width=tile.width,
412
- fb_index=0, # Read from display buffer
389
+ duration=0,
390
+ colors=red_colors,
391
+ fb_index=1, # Write to temp buffer
413
392
  )
414
393
 
394
+ # Copy frame buffer 1 to frame buffer 0 (display)
395
+ await matrix.copy_frame_buffer(tile_index=0, source_fb=1, target_fb=0)
396
+
397
+ # Get the updated colors
398
+ colors: list[HSBK] = []
399
+ colors.extend(await matrix.get64())
400
+ colors.extend(await matrix.get64(y=4))
401
+
415
402
  # Verify the colors were set correctly
416
- assert len(colors) == 64
403
+ assert len(colors) == 128
417
404
  # Blue is hue ~240
418
405
  assert 230 < colors[0].hue < 250
419
406
  assert colors[0].saturation > 0.9 # High saturation
420
407
  assert colors[0].brightness > 0.9 # Full brightness
421
-
422
- # Note: We can't easily verify this without copying to display buffer
423
- # since get64 reads from display buffer (fb_index=0)
424
- # This test verifies the operation completes without error
408
+ # Red is hue ~0
409
+ assert colors[64].hue == 0
410
+ assert colors[64].saturation == 1.0
411
+ assert colors[64].brightness == 1.0
425
412
 
426
413
  async def test_set_matrix_colors_large_tile(self, ceiling_device) -> None:
427
- """Test setting all colors on 16x8 tile (128 zones) using frame buffer strategy.
414
+ """Test setting all colors on 16x8 tile (128 zones) set_matrix_colors()
428
415
 
429
416
  This tests the automatic frame buffer strategy for tiles with >64 zones.
430
417
  The method should automatically batch the colors and use the frame buffer.
@@ -439,7 +426,7 @@ class TestMatrixLight:
439
426
  assert tile.total_zones == 128
440
427
 
441
428
  # Create a gradient (different colors, so uses set64 not SetColor)
442
- colors = [HSBK(i * 360.0 / 128, 1.0, 1.0, 3500) for i in range(128)]
429
+ colors = [HSBK(round(i * 360.0 / 128), 1.0, 1.0, 3500) for i in range(128)]
443
430
 
444
431
  # Set all 128 zones at once (should use frame buffer strategy)
445
432
  # This requires:
@@ -449,13 +436,7 @@ class TestMatrixLight:
449
436
  await matrix.set_matrix_colors(tile_index=0, colors=colors, duration=0)
450
437
 
451
438
  # Verify first 64 zones
452
- first_half = await matrix.get64(
453
- tile_index=0,
454
- length=1,
455
- x=0,
456
- y=0,
457
- width=tile.width,
458
- )
439
+ first_half = await matrix.get64()
459
440
 
460
441
  assert len(first_half) == 64
461
442
  # First zone should be hue 0 (red)
@@ -464,13 +445,7 @@ class TestMatrixLight:
464
445
  assert first_half[0].brightness > 0.9 # Full brightness
465
446
 
466
447
  # Verify second 64 zones
467
- second_half = await matrix.get64(
468
- tile_index=0,
469
- length=1,
470
- x=0,
471
- y=4, # Start at row 4
472
- width=tile.width,
473
- )
448
+ second_half = await matrix.get64(y=4) # Start at row 4
474
449
 
475
450
  assert len(second_half) == 64
476
451
  # Zone 64 should be hue ~180 (cyan)
@@ -508,9 +483,9 @@ class TestMatrixLight:
508
483
  tile_index=0, colors=white_colors, duration=0
509
484
  )
510
485
 
511
- # Create gradient: first 64 zones blue, second 64 zones green
512
- blue_colors = [HSBK.from_rgb(0, 0, 255)] * 64
513
- green_colors = [HSBK.from_rgb(0, 255, 0)] * 64
486
+ # Create colors: first 64 zones blue, second 64 zones green
487
+ blue_colors = [Colors.BLUE] * 64
488
+ green_colors = [Colors.GREEN] * 64
514
489
 
515
490
  # Step 1: Set first 64 zones (rows 0-3) to blue in frame buffer 1
516
491
  await matrix.set64(
@@ -540,25 +515,11 @@ class TestMatrixLight:
540
515
  # This should copy all 128 zones using the tile's width and height
541
516
  await matrix.copy_frame_buffer(tile_index=0, source_fb=1, target_fb=0)
542
517
 
543
- # Step 4: Read back first 64 zones from frame buffer 0 (display)
544
- first_64_colors = await matrix.get64(
545
- tile_index=0,
546
- length=1,
547
- x=0,
548
- y=0,
549
- width=tile.width,
550
- fb_index=0, # Read from display buffer
551
- )
518
+ # Step 4: Read back first 64 zones
519
+ first_64_colors = await matrix.get64()
552
520
 
553
- # Step 5: Read back second 64 zones from frame buffer 0 (display)
554
- second_64_colors = await matrix.get64(
555
- tile_index=0,
556
- length=1,
557
- x=0,
558
- y=4,
559
- width=tile.width,
560
- fb_index=0, # Read from display buffer
561
- )
521
+ # Step 5: Read back second 64 zones
522
+ second_64_colors = await matrix.get64(y=4)
562
523
 
563
524
  # Verify all 128 zones were retrieved
564
525
  assert len(first_64_colors) == 64
@@ -13,6 +13,11 @@ from lifx.effects.const import DEFAULT_BRIGHTNESS
13
13
  class ConcreteEffect(LIFXEffect):
14
14
  """Concrete implementation for testing abstract base class."""
15
15
 
16
+ @property
17
+ def name(self) -> str:
18
+ """Return the name of the effect."""
19
+ return "test"
20
+
16
21
  async def async_play(self) -> None:
17
22
  """Minimal implementation for testing."""
18
23
  pass