lifx-async 5.0.0__tar.gz → 5.1.0__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 (186) hide show
  1. {lifx_async-5.0.0 → lifx_async-5.1.0}/.github/workflows/ci.yml +47 -5
  2. {lifx_async-5.0.0 → lifx_async-5.1.0}/.github/workflows/docs.yml +3 -3
  3. {lifx_async-5.0.0 → lifx_async-5.1.0}/CLAUDE.md +85 -1
  4. {lifx_async-5.0.0 → lifx_async-5.1.0}/PKG-INFO +1 -1
  5. lifx_async-5.1.0/docs/api/animation.md +356 -0
  6. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/index.md +22 -0
  7. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/protocol.md +0 -14
  8. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/changelog.md +29 -0
  9. lifx_async-5.1.0/docs/user-guide/animation.md +369 -0
  10. lifx_async-5.1.0/examples/05_device_groups.py +205 -0
  11. lifx_async-5.1.0/examples/15_animation.py +372 -0
  12. lifx_async-5.1.0/examples/16_animation_numpy.py +543 -0
  13. {lifx_async-5.0.0 → lifx_async-5.1.0}/mkdocs.yml +4 -0
  14. {lifx_async-5.0.0 → lifx_async-5.1.0}/pyproject.toml +1 -1
  15. {lifx_async-5.0.0 → lifx_async-5.1.0}/renovate.json +1 -0
  16. lifx_async-5.1.0/scripts/test_multiversion.py +227 -0
  17. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/__init__.py +4 -0
  18. lifx_async-5.1.0/src/lifx/animation/__init__.py +87 -0
  19. lifx_async-5.1.0/src/lifx/animation/animator.py +323 -0
  20. lifx_async-5.1.0/src/lifx/animation/framebuffer.py +395 -0
  21. lifx_async-5.1.0/src/lifx/animation/orientation.py +159 -0
  22. lifx_async-5.1.0/src/lifx/animation/packets.py +497 -0
  23. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/api.py +0 -8
  24. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/const.py +18 -0
  25. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/ceiling.py +7 -8
  26. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/matrix.py +2 -19
  27. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/colorloop.py +2 -2
  28. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/connection.py +8 -7
  29. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/discovery.py +3 -3
  30. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/mdns/transport.py +2 -2
  31. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/transport.py +8 -3
  32. lifx_async-5.1.0/src/lifx/network/utils.py +15 -0
  33. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/serializer.py +0 -85
  34. lifx_async-5.1.0/tests/test_animation/__init__.py +1 -0
  35. lifx_async-5.1.0/tests/test_animation/conftest.py +178 -0
  36. lifx_async-5.1.0/tests/test_animation/test_animator.py +482 -0
  37. lifx_async-5.1.0/tests/test_animation/test_framebuffer.py +380 -0
  38. lifx_async-5.1.0/tests/test_animation/test_orientation.py +167 -0
  39. lifx_async-5.1.0/tests/test_animation/test_packets.py +504 -0
  40. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_api/test_api_apply_theme.py +2 -1
  41. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_api/test_api_batch_operations.py +4 -0
  42. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_base.py +14 -0
  43. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_multizone.py +8 -4
  44. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_concurrent_requests.py +36 -16
  45. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_protocol/test_serializer.py +0 -161
  46. {lifx_async-5.0.0 → lifx_async-5.1.0}/uv.lock +4 -4
  47. {lifx_async-5.0.0 → lifx_async-5.1.0}/.claude/settings.json +0 -0
  48. {lifx_async-5.0.0 → lifx_async-5.1.0}/.github/dependabot.yml +0 -0
  49. {lifx_async-5.0.0 → lifx_async-5.1.0}/.github/labeler.yml +0 -0
  50. {lifx_async-5.0.0 → lifx_async-5.1.0}/.github/workflows/pr-automation.yml +0 -0
  51. {lifx_async-5.0.0 → lifx_async-5.1.0}/.gitignore +0 -0
  52. {lifx_async-5.0.0 → lifx_async-5.1.0}/.pre-commit-config.yaml +0 -0
  53. {lifx_async-5.0.0 → lifx_async-5.1.0}/LICENSE +0 -0
  54. {lifx_async-5.0.0 → lifx_async-5.1.0}/README.md +0 -0
  55. {lifx_async-5.0.0 → lifx_async-5.1.0}/context7.json +0 -0
  56. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/colors.md +0 -0
  57. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/devices.md +0 -0
  58. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/effects.md +0 -0
  59. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/exceptions.md +0 -0
  60. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/high-level.md +0 -0
  61. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/network.md +0 -0
  62. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/themes.md +0 -0
  63. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/architecture/effects-architecture.md +0 -0
  64. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/architecture/overview.md +0 -0
  65. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/faq.md +0 -0
  66. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/getting-started/effects.md +0 -0
  67. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/getting-started/installation.md +0 -0
  68. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/getting-started/quickstart.md +0 -0
  69. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/getting-started/themes.md +0 -0
  70. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/index.md +0 -0
  71. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/migration/effect-api-changes.md +0 -0
  72. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/stylesheets/extra.css +0 -0
  73. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/advanced-usage.md +0 -0
  74. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/ceiling-lights.md +0 -0
  75. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/effects-custom.md +0 -0
  76. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/effects-troubleshooting.md +0 -0
  77. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/protocol-deep-dive.md +0 -0
  78. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/themes.md +0 -0
  79. {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/troubleshooting.md +0 -0
  80. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/01_simple_discovery.py +0 -0
  81. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/02_simple_control.py +0 -0
  82. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/03_waveforms.py +0 -0
  83. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/04_logging.py +0 -0
  84. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/06_pulse_effect.py +0 -0
  85. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/07_colorloop_effect.py +0 -0
  86. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/08_custom_effect.py +0 -0
  87. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/09_background_effect.py +0 -0
  88. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/10_find_specific_devices.py +0 -0
  89. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/11_matrix_basic.py +0 -0
  90. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/12_matrix_effects.py +0 -0
  91. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/13_matrix_large.py +0 -0
  92. {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/14_mdns_discovery.py +0 -0
  93. {lifx_async-5.0.0 → lifx_async-5.1.0}/scripts/mdns_probe.py +0 -0
  94. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/color.py +0 -0
  95. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/__init__.py +0 -0
  96. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/base.py +0 -0
  97. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/hev.py +0 -0
  98. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/infrared.py +0 -0
  99. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/light.py +0 -0
  100. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/multizone.py +0 -0
  101. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/__init__.py +0 -0
  102. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/base.py +0 -0
  103. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/conductor.py +0 -0
  104. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/const.py +0 -0
  105. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/models.py +0 -0
  106. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/pulse.py +0 -0
  107. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/state_manager.py +0 -0
  108. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/exceptions.py +0 -0
  109. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/__init__.py +0 -0
  110. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/mdns/__init__.py +0 -0
  111. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/mdns/discovery.py +0 -0
  112. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/mdns/dns.py +0 -0
  113. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/mdns/types.py +0 -0
  114. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/message.py +0 -0
  115. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/products/__init__.py +0 -0
  116. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/products/generator.py +0 -0
  117. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/products/quirks.py +0 -0
  118. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/products/registry.py +0 -0
  119. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/__init__.py +0 -0
  120. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/base.py +0 -0
  121. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/generator.py +0 -0
  122. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/header.py +0 -0
  123. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/models.py +0 -0
  124. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/packets.py +0 -0
  125. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/protocol_types.py +0 -0
  126. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/py.typed +0 -0
  127. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/theme/__init__.py +0 -0
  128. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/theme/canvas.py +0 -0
  129. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/theme/generators.py +0 -0
  130. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/theme/library.py +0 -0
  131. {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/theme/theme.py +0 -0
  132. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/__init__.py +0 -0
  133. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/conftest.py +0 -0
  134. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_api/__init__.py +0 -0
  135. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_api/test_api_batch_errors.py +0 -0
  136. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_api/test_api_discovery.py +0 -0
  137. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_api/test_api_organization.py +0 -0
  138. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_color.py +0 -0
  139. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/__init__.py +0 -0
  140. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/conftest.py +0 -0
  141. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_ceiling.py +0 -0
  142. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_hev.py +0 -0
  143. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_infrared.py +0 -0
  144. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_light.py +0 -0
  145. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_mac_address.py +0 -0
  146. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_matrix.py +0 -0
  147. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_multizone.py +0 -0
  148. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_ceiling.py +0 -0
  149. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_hev.py +0 -0
  150. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_infrared.py +0 -0
  151. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_light.py +0 -0
  152. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_management.py +0 -0
  153. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_matrix.py +0 -0
  154. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/__init__.py +0 -0
  155. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_base.py +0 -0
  156. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_capability_filtering.py +0 -0
  157. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_colorloop.py +0 -0
  158. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_integration.py +0 -0
  159. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_models.py +0 -0
  160. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_pulse.py +0 -0
  161. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_state_manager.py +0 -0
  162. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/__init__.py +0 -0
  163. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_connection.py +0 -0
  164. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_discovery_devices.py +0 -0
  165. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_discovery_errors.py +0 -0
  166. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_mdns/__init__.py +0 -0
  167. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_mdns/conftest.py +0 -0
  168. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_mdns/test_discovery.py +0 -0
  169. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_mdns/test_dns.py +0 -0
  170. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_mdns/test_transport.py +0 -0
  171. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_message.py +0 -0
  172. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_message_advanced.py +0 -0
  173. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_transport.py +0 -0
  174. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_products/test_product_generator.py +0 -0
  175. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_products/test_registry.py +0 -0
  176. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_protocol/test_generated.py +0 -0
  177. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_protocol/test_header.py +0 -0
  178. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_protocol/test_protocol_generator.py +0 -0
  179. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/__init__.py +0 -0
  180. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/conftest.py +0 -0
  181. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/test_apply_theme.py +0 -0
  182. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/test_canvas.py +0 -0
  183. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/test_generators.py +0 -0
  184. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/test_library.py +0 -0
  185. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/test_theme.py +0 -0
  186. {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_utils.py +0 -0
@@ -35,7 +35,7 @@ jobs:
35
35
  - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
36
36
 
37
37
  - name: Set up Python
38
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
38
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
39
39
  with:
40
40
  python-version: ${{ env.PYTHON_VERSION }}
41
41
 
@@ -62,6 +62,47 @@ jobs:
62
62
  uv pip install "bandit[toml]"
63
63
  uv run bandit -c pyproject.toml -r src/
64
64
 
65
+ # Verify auto-generated files are up-to-date
66
+ generated-files:
67
+ name: Verify Generated Files
68
+ needs: quality
69
+ runs-on: ubuntu-latest
70
+ steps:
71
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
72
+
73
+ - name: Set up Python
74
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
75
+ with:
76
+ python-version: ${{ env.PYTHON_VERSION }}
77
+
78
+ - name: Install uv
79
+ uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7
80
+ with:
81
+ version: ${{ env.UV_VERSION }}
82
+ python-version: ${{ env.PYTHON_VERSION }}
83
+
84
+ - name: Install dependencies
85
+ run: uv sync --frozen
86
+
87
+ - name: Regenerate protocol files
88
+ run: uv run python -m lifx.protocol.generator
89
+
90
+ - name: Regenerate product registry
91
+ run: uv run python -m lifx.products.generator
92
+
93
+ - name: Format and lint generated files
94
+ run: |
95
+ uv run ruff format src/lifx/protocol/packets.py src/lifx/protocol/protocol_types.py src/lifx/products/registry.py
96
+ uv run ruff check --fix src/lifx/protocol/packets.py src/lifx/protocol/protocol_types.py src/lifx/products/registry.py
97
+
98
+ - name: Check for differences
99
+ run: |
100
+ if ! git diff --exit-code src/lifx/protocol/packets.py src/lifx/protocol/protocol_types.py src/lifx/products/registry.py; then
101
+ echo "::error::Generated files do not match committed versions. See diff above for details."
102
+ exit 1
103
+ fi
104
+ echo "All generated files are up-to-date."
105
+
65
106
  # Testing matrix across Python versions and platforms
66
107
  test:
67
108
  name: Test (Python ${{ matrix.python-version }} on ${{ matrix.os }})
@@ -72,12 +113,12 @@ jobs:
72
113
  fail-fast: false
73
114
  matrix:
74
115
  os: [ubuntu-latest, macos-latest, windows-latest]
75
- python-version: ['3.11', '3.12', '3.13', '3.14']
116
+ python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
76
117
  steps:
77
118
  - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
78
119
 
79
120
  - name: Set up Python ${{ matrix.python-version }}
80
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
121
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
81
122
  with:
82
123
  python-version: ${{ matrix.python-version }}
83
124
 
@@ -129,7 +170,7 @@ jobs:
129
170
  - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
130
171
 
131
172
  - name: Set up Python
132
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
173
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
133
174
  with:
134
175
  python-version: ${{ env.PYTHON_VERSION }}
135
176
 
@@ -167,6 +208,7 @@ jobs:
167
208
  release:
168
209
  needs:
169
210
  - quality
211
+ - generated-files
170
212
  - test
171
213
  # Run release if not cancelled AND (manual dispatch OR quality passed with tests passed/skipped)
172
214
  if: ${{ !cancelled() && (github.event_name == 'workflow_dispatch' || (needs.quality.result == 'success' && (needs.test.result == 'success' || needs.test.result == 'skipped'))) }}
@@ -193,7 +235,7 @@ jobs:
193
235
  ssh-key: ${{ secrets.DEPLOY_KEY }}
194
236
 
195
237
  - name: Set up Python
196
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
238
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
197
239
  with:
198
240
  python-version: ${{ env.PYTHON_VERSION }}
199
241
 
@@ -30,7 +30,7 @@ jobs:
30
30
  fetch-depth: 0 # Fetch all history for git-revision-date-localized
31
31
 
32
32
  - name: Set up Python
33
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
33
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
34
34
  with:
35
35
  python-version: ${{ env.PYTHON_VERSION }}
36
36
 
@@ -64,7 +64,7 @@ jobs:
64
64
  fetch-depth: 0
65
65
 
66
66
  - name: Set up Python
67
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
67
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
68
68
  with:
69
69
  python-version: ${{ env.PYTHON_VERSION }}
70
70
 
@@ -92,7 +92,7 @@ jobs:
92
92
  - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
93
93
 
94
94
  - name: Set up Python
95
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
95
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
96
96
  with:
97
97
  python-version: ${{ env.PYTHON_VERSION }}
98
98
 
@@ -149,7 +149,17 @@ uv run mkdocs gh-deploy
149
149
  - `DeviceGroup`: Batch operations (set_power, set_color, etc.)
150
150
  - `LocationGrouping` / `GroupGrouping`: Organizational structures for location/group-based grouping
151
151
 
152
- 5. **Utilities**
152
+ 5. **Animation Layer** (`src/lifx/animation/`)
153
+
154
+ - `animator.py`: High-level `Animator` class with direct UDP sending
155
+ - `framebuffer.py`: Multi-tile canvas mapping and orientation correction
156
+ - `packets.py`: Prebaked packet templates (`MatrixPacketGenerator`, `MultiZonePacketGenerator`)
157
+ - `orientation.py`: Tile orientation remapping with LRU-cached lookup tables
158
+ - Optimized for high-frequency frame delivery (30+ FPS) for real-time effects
159
+ - Uses protocol-ready uint16 HSBK values (no conversion overhead)
160
+ - Multi-tile canvas support using `user_x`/`user_y` tile positions
161
+
162
+ 6. **Utilities**
153
163
 
154
164
  - `color.py`: `HSBK` class with RGB conversion, `Colors` presets
155
165
  - `const.py`: Critical constants (network settings, UUIDs, official URLs)
@@ -481,6 +491,80 @@ for frame in animation_frames:
481
491
 
482
492
  **Note:** `MatrixLight.set64()` is already fire-and-forget by default.
483
493
 
494
+ ### Animation Module (High-Frequency Frame Delivery)
495
+
496
+ For real-time effects and applications that need to push color data at 30+ FPS, use the animation module:
497
+
498
+ ```python
499
+ from lifx import Animator, MatrixLight
500
+
501
+ async with await MatrixLight.from_ip("192.168.1.100") as device:
502
+ # Create animator for matrix device
503
+ animator = await Animator.for_matrix(device)
504
+
505
+ # Device connection closed - animator sends via direct UDP
506
+ while running:
507
+ # Generate HSBK frame (protocol-ready uint16 values)
508
+ # H/S/B: 0-65535, K: 1500-9000
509
+ hsbk_frame = [(65535, 65535, 65535, 3500)] * animator.pixel_count
510
+
511
+ # send_frame() is synchronous for speed
512
+ stats = animator.send_frame(hsbk_frame)
513
+ print(f"Sent {stats.packets_sent} packets")
514
+
515
+ await asyncio.sleep(1 / 30) # 30 FPS
516
+
517
+ animator.close()
518
+ ```
519
+
520
+ **Key Features:**
521
+ - **Direct UDP**: Bypasses connection layer for maximum throughput
522
+ - **Prebaked packets**: Templates created once, only colors updated per frame
523
+ - **Multi-tile canvas**: Unified coordinate space for multi-tile devices (e.g., 5-tile LIFX Tile)
524
+ - **Tile orientation**: Automatic pixel remapping for rotated tiles
525
+
526
+ **Multi-Tile Canvas:**
527
+
528
+ For devices with multiple tiles, the animator creates a unified canvas based on tile positions:
529
+
530
+ ```python
531
+ async with await MatrixLight.from_ip("192.168.1.100") as device:
532
+ animator = await Animator.for_matrix(device)
533
+
534
+ # For 5 horizontal tiles: canvas is 40x8 (320 pixels)
535
+ print(f"Canvas: {animator.canvas_width}x{animator.canvas_height}")
536
+
537
+ # Generate frame for entire canvas (row-major order)
538
+ frame = []
539
+ for y in range(animator.canvas_height):
540
+ for x in range(animator.canvas_width):
541
+ hue = int(x / animator.canvas_width * 65535) # Rainbow across all tiles
542
+ frame.append((hue, 65535, 65535, 3500))
543
+
544
+ animator.send_frame(frame)
545
+ ```
546
+
547
+ **HSBK Format (Protocol-Ready):**
548
+ ```python
549
+ # (hue, saturation, brightness, kelvin)
550
+ # H/S/B: 0-65535, K: 1500-9000
551
+ red = (0, 65535, 65535, 3500) # Full red
552
+ blue = (43690, 65535, 65535, 3500) # Full blue (240/360 * 65535)
553
+ white = (0, 0, 65535, 5500) # Daylight white
554
+ off = (0, 0, 0, 3500) # Off (black)
555
+ ```
556
+
557
+ **For MultiZone devices (strips/beams):**
558
+ ```python
559
+ from lifx import Animator, MultiZoneLight
560
+
561
+ async with await MultiZoneLight.from_ip("192.168.1.100") as device:
562
+ animator = await Animator.for_multizone(device)
563
+
564
+ # Same API as matrix
565
+ stats = animator.send_frame(hsbk_frame)
566
+ ```
567
+
484
568
  ### Packet Flow
485
569
 
486
570
  1. Create packet instance (e.g., `LightSetColor`)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-async
3
- Version: 5.0.0
3
+ Version: 5.1.0
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>
@@ -0,0 +1,356 @@
1
+ # Animation Module
2
+
3
+ The animation module provides efficient high-frequency frame delivery for LIFX devices, optimized
4
+ for real-time effects and applications that need to push color data at 30+ FPS.
5
+
6
+ ## Overview
7
+
8
+ The animation system uses a streamlined architecture optimized for speed:
9
+
10
+ ```
11
+ Application Frame -> FrameBuffer -> PacketGenerator -> Direct UDP
12
+ (canvas map) (prebaked packets) (fire-and-forget)
13
+ ```
14
+
15
+ Key features:
16
+
17
+ - **Direct UDP**: Bypasses connection layer for maximum throughput
18
+ - **Prebaked packets**: Templates created once, only colors updated per frame
19
+ - **Multi-tile canvas**: Unified coordinate space for multi-tile devices
20
+ - **Tile orientation**: Automatic pixel remapping for rotated tiles
21
+ - **Synchronous sending**: `send_frame()` is synchronous for minimum overhead
22
+
23
+ ## Quick Start
24
+
25
+ ```python
26
+ import asyncio
27
+ from lifx import Animator, MatrixLight
28
+
29
+ async def main():
30
+ async with await MatrixLight.from_ip("192.168.1.100") as device:
31
+ # Create animator for matrix device
32
+ animator = await Animator.for_matrix(device)
33
+
34
+ # Device connection closed - animator sends via direct UDP
35
+ try:
36
+ while True:
37
+ # Generate HSBK frame (protocol-ready uint16 values)
38
+ # H/S/B: 0-65535, K: 1500-9000
39
+ hsbk_frame = [(65535, 65535, 65535, 3500)] * animator.pixel_count
40
+
41
+ # send_frame() is synchronous for speed
42
+ stats = animator.send_frame(hsbk_frame)
43
+ print(f"Sent {stats.packets_sent} packets in {stats.total_time_ms:.2f}ms")
44
+
45
+ await asyncio.sleep(1 / 30) # 30 FPS
46
+ finally:
47
+ animator.close()
48
+ ```
49
+
50
+ ## Multi-Tile Canvas
51
+
52
+ For devices with multiple tiles (like the original 5-tile LIFX Tile), the animator creates
53
+ a unified canvas based on tile positions (`user_x`, `user_y`). Animations span all tiles
54
+ as one continuous image.
55
+
56
+ ```python
57
+ async with await MatrixLight.from_ip("192.168.1.100") as device:
58
+ animator = await Animator.for_matrix(device)
59
+
60
+ # Check canvas dimensions
61
+ print(f"Canvas: {animator.canvas_width}x{animator.canvas_height}")
62
+ # For 5 horizontal tiles: "Canvas: 40x8"
63
+
64
+ # Generate frame for entire canvas (row-major order)
65
+ frame = []
66
+ for y in range(animator.canvas_height):
67
+ for x in range(animator.canvas_width):
68
+ hue = int(x / animator.canvas_width * 65535) # Gradient across all tiles
69
+ frame.append((hue, 65535, 65535, 3500))
70
+
71
+ animator.send_frame(frame)
72
+ ```
73
+
74
+ ## HSBK Format
75
+
76
+ All color data uses protocol-ready uint16 values:
77
+
78
+ | Component | Range | Description |
79
+ |-----------|-------|-------------|
80
+ | Hue | 0-65535 | Maps to 0-360 degrees |
81
+ | Saturation | 0-65535 | Maps to 0.0-1.0 |
82
+ | Brightness | 0-65535 | Maps to 0.0-1.0 |
83
+ | Kelvin | 1500-9000 | Color temperature |
84
+
85
+ This design pushes conversion work to the caller (e.g. using NumPy) for better performance.
86
+ The `lifx-async` library remains dependency-free.
87
+
88
+ ```python
89
+ # Red at full brightness
90
+ red = (0, 65535, 65535, 3500)
91
+
92
+ # 50% brightness warm white
93
+ warm_white = (0, 0, 32768, 2700)
94
+
95
+ # Convert from user-friendly values
96
+ def to_protocol_hsbk(
97
+ hue: float, sat: float, bright: float, kelvin: int
98
+ ) -> tuple[int, int, int, int]:
99
+ """Convert user-friendly values to protocol format."""
100
+ return (
101
+ int(hue / 360 * 65535),
102
+ int(sat * 65535),
103
+ int(bright * 65535),
104
+ kelvin,
105
+ )
106
+ ```
107
+
108
+ ## Animator
109
+
110
+ High-level class integrating all animation components.
111
+
112
+ ::: lifx.animation.animator.Animator
113
+ options:
114
+ show_root_heading: true
115
+ heading_level: 3
116
+ members_order: source
117
+ show_if_no_docstring: false
118
+ filters:
119
+ - "!^_"
120
+
121
+ ### AnimatorStats
122
+
123
+ Statistics returned by `Animator.send_frame()`.
124
+
125
+ ::: lifx.animation.animator.AnimatorStats
126
+ options:
127
+ show_root_heading: true
128
+ heading_level: 4
129
+ members_order: source
130
+ show_if_no_docstring: false
131
+
132
+ ## FrameBuffer
133
+
134
+ Canvas mapping and orientation handling for matrix devices.
135
+
136
+ ::: lifx.animation.framebuffer.FrameBuffer
137
+ options:
138
+ show_root_heading: true
139
+ heading_level: 3
140
+ members_order: source
141
+ show_if_no_docstring: false
142
+ filters:
143
+ - "!^_"
144
+
145
+ ### TileRegion
146
+
147
+ Represents a tile's region within the canvas.
148
+
149
+ ::: lifx.animation.framebuffer.TileRegion
150
+ options:
151
+ show_root_heading: true
152
+ heading_level: 4
153
+ members_order: source
154
+ show_if_no_docstring: false
155
+
156
+ ## Packet Generators
157
+
158
+ Device-specific packet generation with prebaked templates.
159
+
160
+ ### PacketGenerator (Base)
161
+
162
+ ::: lifx.animation.packets.PacketGenerator
163
+ options:
164
+ show_root_heading: true
165
+ heading_level: 4
166
+ members_order: source
167
+ show_if_no_docstring: false
168
+
169
+ ### PacketTemplate
170
+
171
+ Prebaked packet template for zero-allocation frame updates.
172
+
173
+ ::: lifx.animation.packets.PacketTemplate
174
+ options:
175
+ show_root_heading: true
176
+ heading_level: 4
177
+ members_order: source
178
+ show_if_no_docstring: false
179
+
180
+ ### MatrixPacketGenerator
181
+
182
+ Generates Set64 packets for MatrixLight devices.
183
+
184
+ ::: lifx.animation.packets.MatrixPacketGenerator
185
+ options:
186
+ show_root_heading: true
187
+ heading_level: 4
188
+ members_order: source
189
+ show_if_no_docstring: false
190
+ filters:
191
+ - "!^_"
192
+
193
+ ### MultiZonePacketGenerator
194
+
195
+ Generates SetExtendedColorZones packets for MultiZoneLight devices.
196
+
197
+ ::: lifx.animation.packets.MultiZonePacketGenerator
198
+ options:
199
+ show_root_heading: true
200
+ heading_level: 4
201
+ members_order: source
202
+ show_if_no_docstring: false
203
+ filters:
204
+ - "!^_"
205
+
206
+ ## Tile Orientation
207
+
208
+ Pixel remapping for rotated tiles.
209
+
210
+ ### Orientation Enum
211
+
212
+ ::: lifx.animation.orientation.Orientation
213
+ options:
214
+ show_root_heading: true
215
+ heading_level: 4
216
+ members_order: source
217
+ show_if_no_docstring: false
218
+
219
+ ### build_orientation_lut
220
+
221
+ ::: lifx.animation.orientation.build_orientation_lut
222
+ options:
223
+ show_root_heading: true
224
+ heading_level: 4
225
+
226
+ ## Examples
227
+
228
+ ### Matrix Animation (Single Tile)
229
+
230
+ ```python
231
+ import asyncio
232
+ from lifx import Animator, MatrixLight
233
+
234
+ async def rainbow_animation():
235
+ async with await MatrixLight.from_ip("192.168.1.100") as device:
236
+ animator = await Animator.for_matrix(device)
237
+
238
+ hue_offset = 0
239
+ try:
240
+ while True:
241
+ # Generate rainbow gradient
242
+ frame = []
243
+ for i in range(animator.pixel_count):
244
+ hue = (hue_offset + i * 1000) % 65536
245
+ frame.append((hue, 65535, 32768, 3500))
246
+
247
+ stats = animator.send_frame(frame)
248
+ print(f"Sent {stats.packets_sent} packets")
249
+
250
+ hue_offset = (hue_offset + 500) % 65536
251
+ await asyncio.sleep(1 / 30) # 30 FPS
252
+ finally:
253
+ animator.close()
254
+ ```
255
+
256
+ ### Multi-Tile Animation (LIFX Tile with 5 tiles)
257
+
258
+ ```python
259
+ import asyncio
260
+ import math
261
+ from lifx import Animator, MatrixLight
262
+
263
+ async def multi_tile_wave():
264
+ async with await MatrixLight.from_ip("192.168.1.100") as device:
265
+ animator = await Animator.for_matrix(device)
266
+
267
+ # Canvas spans all tiles (e.g., 40x8 for 5 horizontal tiles)
268
+ width = animator.canvas_width
269
+ height = animator.canvas_height
270
+ print(f"Canvas: {width}x{height}")
271
+
272
+ hue_offset = 0
273
+ try:
274
+ while True:
275
+ frame = []
276
+ for y in range(height):
277
+ for x in range(width):
278
+ # Wave that flows across all tiles
279
+ pos = x + y * 0.5 # Diagonal wave
280
+ hue = int((pos / width) * 65535 + hue_offset) % 65536
281
+ frame.append((hue, 65535, 65535, 3500))
282
+
283
+ animator.send_frame(frame)
284
+ hue_offset = (hue_offset + 1000) % 65536
285
+ await asyncio.sleep(1 / 30)
286
+ finally:
287
+ animator.close()
288
+ ```
289
+
290
+ ### MultiZone Animation
291
+
292
+ ```python
293
+ import asyncio
294
+ from lifx import Animator, MultiZoneLight
295
+
296
+ async def chase_animation():
297
+ async with await MultiZoneLight.from_ip("192.168.1.100") as device:
298
+ animator = await Animator.for_multizone(device)
299
+
300
+ position = 0
301
+ try:
302
+ while True:
303
+ # Generate chase pattern
304
+ frame = []
305
+ for i in range(animator.pixel_count):
306
+ if i == position:
307
+ frame.append((0, 65535, 65535, 3500)) # Red
308
+ else:
309
+ frame.append((0, 0, 0, 3500)) # Off
310
+
311
+ animator.send_frame(frame)
312
+
313
+ position = (position + 1) % animator.pixel_count
314
+ await asyncio.sleep(1 / 20) # 20 FPS
315
+ finally:
316
+ animator.close()
317
+ ```
318
+
319
+ ## Performance Characteristics
320
+
321
+ ### Direct UDP Delivery
322
+
323
+ The animation module bypasses the connection layer entirely:
324
+
325
+ - No ACKs, no waiting, no retries
326
+ - Packets sent via raw UDP socket
327
+ - Maximum throughput for real-time effects
328
+ - Some packet loss is acceptable (visual artifacts are brief)
329
+
330
+ ### Prebaked Packet Templates
331
+
332
+ Packets are constructed once at initialization:
333
+
334
+ - Header and payload structure prebaked as `bytearray`
335
+ - Per-frame: only color data and sequence number updated
336
+ - Zero object allocation in the hot path
337
+ - Sequence number wraps at 256 (uint8)
338
+
339
+ ### Multi-Tile Canvas Mapping
340
+
341
+ For devices with multiple tiles:
342
+
343
+ - Tile positions read from device (`user_x`, `user_y`)
344
+ - Canvas bounds calculated from all tile positions
345
+ - Input frame interpreted as 2D row-major canvas
346
+ - Each tile extracts its region based on position
347
+ - Orientation correction applied per-tile
348
+
349
+ ### Typical Performance
350
+
351
+ | Device Type | Pixels | Packets/Frame | Send Time |
352
+ |-------------|--------|---------------|-----------|
353
+ | Single tile (8x8) | 64 | 1 | <0.5ms |
354
+ | 5-tile chain | 320 | 5 | <1ms |
355
+ | Large Ceiling (16x8) | 128 | 3 | <1ms |
356
+ | MultiZone (82 zones) | 82 | 1 | <0.5ms |
@@ -11,6 +11,12 @@ lifx/
11
11
  ├── color.py # Color utilities (HSBK, Colors)
12
12
  ├── const.py # Network constants and URLs
13
13
  ├── exceptions.py # Exception hierarchy
14
+ ├── animation/ # Animation module for high-frequency frame delivery
15
+ │ ├── __init__.py # Public API exports
16
+ │ ├── animator.py # High-level Animator class with direct UDP
17
+ │ ├── framebuffer.py # Multi-tile canvas mapping and orientation
18
+ │ ├── packets.py # Prebaked packet templates
19
+ │ └── orientation.py # Tile orientation remapping
14
20
  ├── devices/ # Device classes
15
21
  │ ├── base.py # Base Device class
16
22
  │ ├── light.py # Light device (color control)
@@ -73,6 +79,14 @@ Work with colors:
73
79
  - [`HSBK`](colors.md#lifx.color.HSBK) - Color representation
74
80
  - [`Colors`](colors.md#lifx.color.Colors) - Built-in presets
75
81
 
82
+ ### Animation
83
+
84
+ High-frequency frame delivery for real-time effects:
85
+
86
+ - [`Animator`](animation.md#lifx.animation.animator.Animator) - High-level animation interface with direct UDP
87
+ - [`FrameBuffer`](animation.md#lifx.animation.framebuffer.FrameBuffer) - Multi-tile canvas mapping
88
+ - [`PacketTemplate`](animation.md#lifx.animation.packets.PacketTemplate) - Prebaked packet templates
89
+
76
90
  ### Network Layer
77
91
 
78
92
  Low-level network operations:
@@ -195,6 +209,14 @@ async def set_custom_color(light: Light, hue: float) -> None:
195
209
 
196
210
  [:octicons-arrow-right-24: Colors](colors.md)
197
211
 
212
+ - :material-animation:{ .lg .middle } __Animation__
213
+
214
+ ______________________________________________________________________
215
+
216
+ High-frequency frame delivery for real-time effects
217
+
218
+ [:octicons-arrow-right-24: Animation](animation.md)
219
+
198
220
  - :material-network:{ .lg .middle } __Network Layer__
199
221
 
200
222
  ______________________________________________________________________
@@ -30,19 +30,6 @@ The LIFX protocol header structure (36 bytes).
30
30
  members_order: source
31
31
  show_if_no_docstring: false
32
32
 
33
- ## Serializer
34
-
35
- Binary serialization and deserialization utilities.
36
-
37
- ::: lifx.protocol.serializer.FieldSerializer
38
- options:
39
- show_root_heading: true
40
- heading_level: 3
41
- members_order: source
42
- show_if_no_docstring: false
43
- filters:
44
- - "!^_"
45
-
46
33
  ## Protocol Types
47
34
 
48
35
  Common protocol type definitions and enums.
@@ -241,7 +228,6 @@ async def main():
241
228
 
242
229
  ```python
243
230
  from lifx.protocol.packets import DeviceSetLabel
244
- from lifx.protocol.serializer import Serializer
245
231
 
246
232
  # Create packet
247
233
  packet = DeviceSetLabel(label=b"Kitchen Light\0" + b"\0" * 19)
@@ -2,6 +2,35 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v5.1.0 (2026-01-24)
6
+
7
+ ### Bug Fixes
8
+
9
+ - **animation**: Load capabilities before checking has_chain
10
+ ([`69e50b3`](https://github.com/Djelibeybi/lifx-async/commit/69e50b356ed22ad1679a823444db9fa7c19626b7))
11
+
12
+ ### Documentation
13
+
14
+ - Add DeviceGroup usage example
15
+ ([`9f85854`](https://github.com/Djelibeybi/lifx-async/commit/9f858543f745140c154128c63006047fccdbd823))
16
+
17
+ - Remove reference to deleted FieldSerializer class
18
+ ([`5db0262`](https://github.com/Djelibeybi/lifx-async/commit/5db0262b593121be2de00bba44d522e11e449845))
19
+
20
+ ### Features
21
+
22
+ - **animation**: Add high-performance animation module
23
+ ([`afc8063`](https://github.com/Djelibeybi/lifx-async/commit/afc8063e6d863e118377facab30a7d3035b1ded5))
24
+
25
+
26
+ ## v5.0.1 (2026-01-14)
27
+
28
+ ### Bug Fixes
29
+
30
+ - Handle asyncio.TimeoutError on Python 3.10
31
+ ([`4438bc4`](https://github.com/Djelibeybi/lifx-async/commit/4438bc45f19f477b585c6af8cf8cbaf5e9341d14))
32
+
33
+
5
34
  ## v5.0.0 (2026-01-12)
6
35
 
7
36
  ### Features