lifx-async 4.8.1__tar.gz → 5.0.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 (168) hide show
  1. {lifx_async-4.8.1 → lifx_async-5.0.0}/.github/workflows/ci.yml +4 -4
  2. {lifx_async-4.8.1 → lifx_async-5.0.0}/.github/workflows/docs.yml +3 -3
  3. {lifx_async-4.8.1 → lifx_async-5.0.0}/PKG-INFO +3 -2
  4. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/changelog.md +21 -0
  5. {lifx_async-4.8.1 → lifx_async-5.0.0}/pyproject.toml +9 -5
  6. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/api.py +34 -36
  7. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/color.py +223 -23
  8. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/devices/base.py +25 -22
  9. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/devices/hev.py +14 -18
  10. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/devices/light.py +15 -15
  11. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/devices/multizone.py +8 -12
  12. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/effects/base.py +11 -7
  13. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/effects/colorloop.py +4 -10
  14. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/effects/conductor.py +43 -46
  15. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/effects/pulse.py +4 -10
  16. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/network/connection.py +1 -1
  17. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/network/mdns/transport.py +4 -3
  18. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/network/transport.py +70 -63
  19. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/conftest.py +29 -6
  20. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_api/test_api_batch_errors.py +30 -53
  21. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_matrix.py +8 -6
  22. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/test_connection.py +9 -7
  23. {lifx_async-4.8.1 → lifx_async-5.0.0}/uv.lock +127 -6
  24. {lifx_async-4.8.1 → lifx_async-5.0.0}/.claude/settings.json +0 -0
  25. {lifx_async-4.8.1 → lifx_async-5.0.0}/.github/dependabot.yml +0 -0
  26. {lifx_async-4.8.1 → lifx_async-5.0.0}/.github/labeler.yml +0 -0
  27. {lifx_async-4.8.1 → lifx_async-5.0.0}/.github/workflows/pr-automation.yml +0 -0
  28. {lifx_async-4.8.1 → lifx_async-5.0.0}/.gitignore +0 -0
  29. {lifx_async-4.8.1 → lifx_async-5.0.0}/.pre-commit-config.yaml +0 -0
  30. {lifx_async-4.8.1 → lifx_async-5.0.0}/CLAUDE.md +0 -0
  31. {lifx_async-4.8.1 → lifx_async-5.0.0}/LICENSE +0 -0
  32. {lifx_async-4.8.1 → lifx_async-5.0.0}/README.md +0 -0
  33. {lifx_async-4.8.1 → lifx_async-5.0.0}/context7.json +0 -0
  34. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/api/colors.md +0 -0
  35. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/api/devices.md +0 -0
  36. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/api/effects.md +0 -0
  37. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/api/exceptions.md +0 -0
  38. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/api/high-level.md +0 -0
  39. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/api/index.md +0 -0
  40. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/api/network.md +0 -0
  41. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/api/protocol.md +0 -0
  42. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/api/themes.md +0 -0
  43. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/architecture/effects-architecture.md +0 -0
  44. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/architecture/overview.md +0 -0
  45. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/faq.md +0 -0
  46. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/getting-started/effects.md +0 -0
  47. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/getting-started/installation.md +0 -0
  48. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/getting-started/quickstart.md +0 -0
  49. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/getting-started/themes.md +0 -0
  50. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/index.md +0 -0
  51. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/migration/effect-api-changes.md +0 -0
  52. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/stylesheets/extra.css +0 -0
  53. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/user-guide/advanced-usage.md +0 -0
  54. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/user-guide/ceiling-lights.md +0 -0
  55. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/user-guide/effects-custom.md +0 -0
  56. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/user-guide/effects-troubleshooting.md +0 -0
  57. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/user-guide/protocol-deep-dive.md +0 -0
  58. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/user-guide/themes.md +0 -0
  59. {lifx_async-4.8.1 → lifx_async-5.0.0}/docs/user-guide/troubleshooting.md +0 -0
  60. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/01_simple_discovery.py +0 -0
  61. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/02_simple_control.py +0 -0
  62. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/03_waveforms.py +0 -0
  63. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/04_logging.py +0 -0
  64. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/06_pulse_effect.py +0 -0
  65. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/07_colorloop_effect.py +0 -0
  66. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/08_custom_effect.py +0 -0
  67. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/09_background_effect.py +0 -0
  68. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/10_find_specific_devices.py +0 -0
  69. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/11_matrix_basic.py +0 -0
  70. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/12_matrix_effects.py +0 -0
  71. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/13_matrix_large.py +0 -0
  72. {lifx_async-4.8.1 → lifx_async-5.0.0}/examples/14_mdns_discovery.py +0 -0
  73. {lifx_async-4.8.1 → lifx_async-5.0.0}/mkdocs.yml +0 -0
  74. {lifx_async-4.8.1 → lifx_async-5.0.0}/renovate.json +0 -0
  75. {lifx_async-4.8.1 → lifx_async-5.0.0}/scripts/mdns_probe.py +0 -0
  76. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/__init__.py +0 -0
  77. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/const.py +0 -0
  78. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/devices/__init__.py +0 -0
  79. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/devices/ceiling.py +0 -0
  80. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/devices/infrared.py +0 -0
  81. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/devices/matrix.py +0 -0
  82. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/effects/__init__.py +0 -0
  83. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/effects/const.py +0 -0
  84. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/effects/models.py +0 -0
  85. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/effects/state_manager.py +0 -0
  86. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/exceptions.py +0 -0
  87. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/network/__init__.py +0 -0
  88. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/network/discovery.py +0 -0
  89. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/network/mdns/__init__.py +0 -0
  90. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/network/mdns/discovery.py +0 -0
  91. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/network/mdns/dns.py +0 -0
  92. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/network/mdns/types.py +0 -0
  93. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/network/message.py +0 -0
  94. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/products/__init__.py +0 -0
  95. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/products/generator.py +0 -0
  96. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/products/quirks.py +0 -0
  97. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/products/registry.py +0 -0
  98. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/protocol/__init__.py +0 -0
  99. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/protocol/base.py +0 -0
  100. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/protocol/generator.py +0 -0
  101. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/protocol/header.py +0 -0
  102. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/protocol/models.py +0 -0
  103. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/protocol/packets.py +0 -0
  104. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/protocol/protocol_types.py +0 -0
  105. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/protocol/serializer.py +0 -0
  106. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/py.typed +0 -0
  107. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/theme/__init__.py +0 -0
  108. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/theme/canvas.py +0 -0
  109. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/theme/generators.py +0 -0
  110. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/theme/library.py +0 -0
  111. {lifx_async-4.8.1 → lifx_async-5.0.0}/src/lifx/theme/theme.py +0 -0
  112. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/__init__.py +0 -0
  113. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_api/__init__.py +0 -0
  114. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_api/test_api_apply_theme.py +0 -0
  115. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_api/test_api_batch_operations.py +0 -0
  116. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_api/test_api_discovery.py +0 -0
  117. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_api/test_api_organization.py +0 -0
  118. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_color.py +0 -0
  119. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/__init__.py +0 -0
  120. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/conftest.py +0 -0
  121. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_base.py +0 -0
  122. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_ceiling.py +0 -0
  123. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_hev.py +0 -0
  124. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_infrared.py +0 -0
  125. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_light.py +0 -0
  126. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_mac_address.py +0 -0
  127. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_multizone.py +0 -0
  128. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_state_ceiling.py +0 -0
  129. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_state_hev.py +0 -0
  130. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_state_infrared.py +0 -0
  131. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_state_light.py +0 -0
  132. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_state_management.py +0 -0
  133. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_state_matrix.py +0 -0
  134. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_devices/test_state_multizone.py +0 -0
  135. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_effects/__init__.py +0 -0
  136. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_effects/test_base.py +0 -0
  137. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_effects/test_capability_filtering.py +0 -0
  138. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_effects/test_colorloop.py +0 -0
  139. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_effects/test_integration.py +0 -0
  140. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_effects/test_models.py +0 -0
  141. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_effects/test_pulse.py +0 -0
  142. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_effects/test_state_manager.py +0 -0
  143. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/__init__.py +0 -0
  144. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/test_concurrent_requests.py +0 -0
  145. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/test_discovery_devices.py +0 -0
  146. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/test_discovery_errors.py +0 -0
  147. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/test_mdns/__init__.py +0 -0
  148. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/test_mdns/conftest.py +0 -0
  149. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/test_mdns/test_discovery.py +0 -0
  150. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/test_mdns/test_dns.py +0 -0
  151. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/test_mdns/test_transport.py +0 -0
  152. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/test_message.py +0 -0
  153. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/test_message_advanced.py +0 -0
  154. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_network/test_transport.py +0 -0
  155. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_products/test_product_generator.py +0 -0
  156. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_products/test_registry.py +0 -0
  157. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_protocol/test_generated.py +0 -0
  158. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_protocol/test_header.py +0 -0
  159. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_protocol/test_protocol_generator.py +0 -0
  160. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_protocol/test_serializer.py +0 -0
  161. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_theme/__init__.py +0 -0
  162. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_theme/conftest.py +0 -0
  163. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_theme/test_apply_theme.py +0 -0
  164. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_theme/test_canvas.py +0 -0
  165. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_theme/test_generators.py +0 -0
  166. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_theme/test_library.py +0 -0
  167. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_theme/test_theme.py +0 -0
  168. {lifx_async-4.8.1 → lifx_async-5.0.0}/tests/test_utils.py +0 -0
@@ -40,7 +40,7 @@ jobs:
40
40
  python-version: ${{ env.PYTHON_VERSION }}
41
41
 
42
42
  - name: Install uv
43
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
43
+ uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7
44
44
  with:
45
45
  version: ${{ env.UV_VERSION }}
46
46
  python-version: ${{ env.PYTHON_VERSION }}
@@ -82,7 +82,7 @@ jobs:
82
82
  python-version: ${{ matrix.python-version }}
83
83
 
84
84
  - name: Install uv
85
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
85
+ uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7
86
86
  with:
87
87
  version: ${{ env.UV_VERSION }}
88
88
  python-version: ${{ matrix.python-version }}
@@ -134,7 +134,7 @@ jobs:
134
134
  python-version: ${{ env.PYTHON_VERSION }}
135
135
 
136
136
  - name: Install uv
137
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
137
+ uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7
138
138
  with:
139
139
  version: ${{ env.UV_VERSION }}
140
140
  python-version: ${{ env.PYTHON_VERSION }}
@@ -198,7 +198,7 @@ jobs:
198
198
  python-version: ${{ env.PYTHON_VERSION }}
199
199
 
200
200
  - name: Install uv
201
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
201
+ uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7
202
202
  with:
203
203
  version: ${{ env.UV_VERSION }}
204
204
  python-version: ${{ env.PYTHON_VERSION }}
@@ -35,7 +35,7 @@ jobs:
35
35
  python-version: ${{ env.PYTHON_VERSION }}
36
36
 
37
37
  - name: Install uv
38
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
38
+ uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7
39
39
  with:
40
40
  version: ${{ env.UV_VERSION }}
41
41
  python-version: ${{ env.PYTHON_VERSION }}
@@ -69,7 +69,7 @@ jobs:
69
69
  python-version: ${{ env.PYTHON_VERSION }}
70
70
 
71
71
  - name: Install uv
72
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
72
+ uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7
73
73
  with:
74
74
  version: ${{ env.UV_VERSION }}
75
75
  python-version: ${{ env.PYTHON_VERSION }}
@@ -97,7 +97,7 @@ jobs:
97
97
  python-version: ${{ env.PYTHON_VERSION }}
98
98
 
99
99
  - name: Install uv
100
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
100
+ uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7
101
101
  with:
102
102
  version: ${{ env.UV_VERSION }}
103
103
  python-version: ${{ env.PYTHON_VERSION }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-async
3
- Version: 4.8.1
3
+ Version: 5.0.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>
@@ -11,6 +11,7 @@ Classifier: Framework :: Pytest
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Natural Language :: English
13
13
  Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3.10
14
15
  Classifier: Programming Language :: Python :: 3.11
15
16
  Classifier: Programming Language :: Python :: 3.12
16
17
  Classifier: Programming Language :: Python :: 3.13
@@ -18,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.14
18
19
  Classifier: Topic :: Software Development :: Libraries
19
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
21
  Classifier: Typing :: Typed
21
- Requires-Python: >=3.11
22
+ Requires-Python: >=3.10
22
23
  Description-Content-Type: text/markdown
23
24
 
24
25
  # lifx-async
@@ -2,6 +2,27 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v5.0.0 (2026-01-12)
6
+
7
+ ### Features
8
+
9
+ - Add Python 3.10 support
10
+ ([`7c39131`](https://github.com/Djelibeybi/lifx-async/commit/7c391314305bb856d8bbcd23a5e481b729a5ad04))
11
+
12
+ ### Breaking Changes
13
+
14
+ - Batch operations now raise first exception immediately (asyncio.gather behavior) instead of
15
+ collecting all exceptions into an ExceptionGroup (TaskGroup behavior).
16
+
17
+
18
+ ## v4.9.0 (2025-12-30)
19
+
20
+ ### Features
21
+
22
+ - **api**: Add HTML named colors and kelvin temperature presets
23
+ ([`b631d43`](https://github.com/Djelibeybi/lifx-async/commit/b631d43f9ba61db5f77e233a6bc0745bb3fed8b8))
24
+
25
+
5
26
  ## v4.8.1 (2025-12-24)
6
27
 
7
28
  ### Bug Fixes
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "lifx-async"
3
- version = "4.8.1"
3
+ version = "5.0.0"
4
4
  description = "A modern, type-safe, async Python library for controlling LIFX lights"
5
5
  readme = "README.md"
6
- requires-python = ">=3.11"
6
+ requires-python = ">=3.10"
7
7
  dependencies = []
8
8
  license = "UPL-1.0"
9
9
  license-files = ["LICENSE"]
@@ -19,6 +19,7 @@ classifiers = [
19
19
  "Intended Audience :: Developers",
20
20
  "Natural Language :: English",
21
21
  "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3.10",
22
23
  "Programming Language :: Python :: 3.11",
23
24
  "Programming Language :: Python :: 3.12",
24
25
  "Programming Language :: Python :: 3.13",
@@ -31,7 +32,7 @@ classifiers = [
31
32
  [dependency-groups]
32
33
  dev = [
33
34
  "hatchling>=1.27.0",
34
- "lifx-emulator-core>=3.0.3",
35
+ "lifx-emulator-core>=3.1.0",
35
36
  "mkdocs-git-revision-date-localized-plugin>=1.4.7",
36
37
  "mkdocs-llmstxt>=0.4.0",
37
38
  "mkdocs-material>=9.6.22",
@@ -40,9 +41,11 @@ dev = [
40
41
  "pytest>=8.4.2",
41
42
  "pytest-asyncio>=0.24.0",
42
43
  "pytest-cov>=7.0.0",
44
+ "pytest-retry>=1.7.0",
43
45
  "pytest-sugar>=1.1.1",
44
46
  "pyyaml>=6.0.3",
45
47
  "ruff>=0.14.2",
48
+ "typing-extensions>=4.15.0",
46
49
  ]
47
50
 
48
51
  [build-system]
@@ -55,7 +58,7 @@ packages = ["src/lifx"]
55
58
  [tool.ruff]
56
59
  line-length = 88
57
60
  indent-width = 4
58
- target-version = "py311"
61
+ target-version = "py310"
59
62
 
60
63
  [tool.ruff.format]
61
64
  quote-style = "double"
@@ -71,10 +74,11 @@ ignore = []
71
74
  "src/lifx/{protocol,products}/generator.py" = ["E501"]
72
75
  "src/lifx/protocol/packets.py" = ["E501"]
73
76
  "src/lifx/products/registry.py" = ["E501"]
77
+ "benchmarks/*.py" = ["E501"]
74
78
 
75
79
  [tool.pyright]
76
80
  typeCheckingMode = "standard"
77
- pythonVersion = "3.11"
81
+ pythonVersion = "3.10"
78
82
  include = ["src"]
79
83
  exclude = [
80
84
  "**/__pycache__",
@@ -198,9 +198,7 @@ class DeviceGroup:
198
198
  await group.set_power(True, duration=1.0)
199
199
  ```
200
200
  """
201
- async with asyncio.TaskGroup() as tg:
202
- for light in self.lights:
203
- tg.create_task(light.set_power(on, duration))
201
+ await asyncio.gather(*(light.set_power(on, duration) for light in self.lights))
204
202
 
205
203
  async def set_color(self, color: HSBK, duration: float = 0.0) -> None:
206
204
  """Set color for all Light devices in the group.
@@ -218,9 +216,9 @@ class DeviceGroup:
218
216
  await group.set_color(HSBK.from_rgb(255, 0, 0), duration=2.0)
219
217
  ```
220
218
  """
221
- async with asyncio.TaskGroup() as tg:
222
- for light in self.lights:
223
- tg.create_task(light.set_color(color, duration))
219
+ await asyncio.gather(
220
+ *(light.set_color(color, duration) for light in self.lights)
221
+ )
224
222
 
225
223
  async def set_brightness(self, brightness: float, duration: float = 0.0) -> None:
226
224
  """Set brightness for all Light devices in the group.
@@ -238,9 +236,9 @@ class DeviceGroup:
238
236
  await group.set_brightness(0.5, duration=1.0)
239
237
  ```
240
238
  """
241
- async with asyncio.TaskGroup() as tg:
242
- for light in self.lights:
243
- tg.create_task(light.set_brightness(brightness, duration))
239
+ await asyncio.gather(
240
+ *(light.set_brightness(brightness, duration) for light in self.lights)
241
+ )
244
242
 
245
243
  async def pulse(
246
244
  self, color: HSBK, period: float = 1.0, cycles: float = 1.0
@@ -261,9 +259,9 @@ class DeviceGroup:
261
259
  await group.pulse(Colors.RED, period=1.0, cycles=1.0)
262
260
  ```
263
261
  """
264
- async with asyncio.TaskGroup() as tg:
265
- for light in self.lights:
266
- tg.create_task(light.pulse(color, period, cycles))
262
+ await asyncio.gather(
263
+ *(light.pulse(color, period, cycles) for light in self.lights)
264
+ )
267
265
 
268
266
  # Location and Group Organization Methods
269
267
 
@@ -280,14 +278,13 @@ class DeviceGroup:
280
278
  )
281
279
 
282
280
  # Fetch all location info concurrently
283
- tasks: dict[str, asyncio.Task[CollectionInfo | None]] = {}
284
- async with asyncio.TaskGroup() as tg:
285
- for device in self._devices:
286
- tasks[device.serial] = tg.create_task(device.get_location())
281
+ location_results = await asyncio.gather(
282
+ *(device.get_location() for device in self._devices)
283
+ )
287
284
 
288
- results: list[tuple[Device, CollectionInfo | None]] = []
289
- for device in self._devices:
290
- results.append((device, tasks[device.serial].result()))
285
+ results: list[tuple[Device, CollectionInfo | None]] = list(
286
+ zip(self._devices, location_results)
287
+ )
291
288
 
292
289
  # Group by location UUID
293
290
  for device, location_info in results:
@@ -332,15 +329,14 @@ class DeviceGroup:
332
329
  # Collect group info from all devices concurrently
333
330
  group_data: dict[str, list[tuple[Device, CollectionInfo]]] = defaultdict(list)
334
331
 
335
- tasks: dict[str, asyncio.Task[CollectionInfo | None]] = {}
336
- async with asyncio.TaskGroup() as tg:
337
- for device in self._devices:
338
- tasks[device.serial] = tg.create_task(device.get_group())
339
-
340
332
  # Fetch all group info concurrently
341
- results: list[tuple[Device, CollectionInfo | None]] = []
342
- for device in self._devices:
343
- results.append((device, tasks[device.serial].result()))
333
+ group_results = await asyncio.gather(
334
+ *(device.get_group() for device in self._devices)
335
+ )
336
+
337
+ results: list[tuple[Device, CollectionInfo | None]] = list(
338
+ zip(self._devices, group_results)
339
+ )
344
340
 
345
341
  # Group by group UUID
346
342
  for device, group_info in results:
@@ -712,18 +708,20 @@ class DeviceGroup:
712
708
  await group.apply_theme(evening, power_on=True, duration=1.0)
713
709
  ```
714
710
  """
715
- async with asyncio.TaskGroup() as tg:
711
+ await asyncio.gather(
716
712
  # Apply theme to all lights
717
- for light in self.lights:
718
- tg.create_task(light.apply_theme(theme, power_on, duration))
719
-
713
+ *(light.apply_theme(theme, power_on, duration) for light in self.lights),
720
714
  # Apply theme to all multizone lights
721
- for multizone in self.multizone_lights:
722
- tg.create_task(multizone.apply_theme(theme, power_on, duration))
723
-
715
+ *(
716
+ multizone.apply_theme(theme, power_on, duration)
717
+ for multizone in self.multizone_lights
718
+ ),
724
719
  # Apply theme to all matrix light devices
725
- for matrix in self.matrix_lights:
726
- tg.create_task(matrix.apply_theme(theme, power_on, duration))
720
+ *(
721
+ matrix.apply_theme(theme, power_on, duration)
722
+ for matrix in self.matrix_lights
723
+ ),
724
+ )
727
725
 
728
726
  def invalidate_metadata_cache(self) -> None:
729
727
  """Clear all cached location and group metadata.
@@ -10,9 +10,23 @@ import colorsys
10
10
  import math
11
11
 
12
12
  from lifx.const import (
13
+ KELVIN_AMBER,
14
+ KELVIN_BLUE_DAYLIGHT,
15
+ KELVIN_BLUE_ICE,
16
+ KELVIN_BLUE_OVERCAST,
17
+ KELVIN_BRIGHT_DAYLIGHT,
18
+ KELVIN_CANDLELIGHT,
19
+ KELVIN_CLOUDY_DAYLIGHT,
13
20
  KELVIN_COOL,
21
+ KELVIN_COOL_DAYLIGHT,
14
22
  KELVIN_DAYLIGHT,
23
+ KELVIN_INCANDESCENT,
15
24
  KELVIN_NEUTRAL,
25
+ KELVIN_NEUTRAL_WARM,
26
+ KELVIN_NOON_DAYLIGHT,
27
+ KELVIN_SOFT_DAYLIGHT,
28
+ KELVIN_SUNSET,
29
+ KELVIN_ULTRA_WARM,
16
30
  KELVIN_WARM,
17
31
  MAX_BRIGHTNESS,
18
32
  MAX_HUE,
@@ -520,34 +534,220 @@ class HSBK:
520
534
 
521
535
  # Common color presets
522
536
  class Colors:
523
- """Common color presets for convenience."""
524
-
525
- # Primary colors
526
- RED = HSBK(hue=0, saturation=1.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
527
- ORANGE = HSBK(hue=30, saturation=1.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
528
- YELLOW = HSBK(hue=60, saturation=1.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
529
- GREEN = HSBK(hue=120, saturation=1.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
530
- CYAN = HSBK(hue=180, saturation=1.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
531
- BLUE = HSBK(hue=240, saturation=1.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
532
- PURPLE = HSBK(hue=270, saturation=1.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
533
- MAGENTA = HSBK(hue=300, saturation=1.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
534
- PINK = HSBK(hue=330, saturation=1.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
535
-
536
- # White variants
537
- WHITE_WARM = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_WARM)
538
- WHITE_NEUTRAL = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
539
- WHITE_COOL = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_COOL)
540
- WHITE_DAYLIGHT = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_DAYLIGHT)
541
-
542
- # Pastels
537
+ """Common color presets for convenience.
538
+
539
+ Includes all 140 standard HTML/CSS named colors plus white temperature
540
+ variants and pastel variations.
541
+
542
+ HTML color reference: https://www.w3.org/TR/css-color-3/#svg-color
543
+ """
544
+
545
+ # Off / Black (brightness=0 turns light off or sets zone to black)
546
+ OFF = HSBK(hue=0, saturation=0.0, brightness=0.0, kelvin=KELVIN_NEUTRAL)
547
+
548
+ # HTML Named Colors (alphabetical order)
549
+ ALICE_BLUE = HSBK.from_rgb(240, 248, 255)
550
+ ANTIQUE_WHITE = HSBK.from_rgb(250, 235, 215)
551
+ AQUA = HSBK.from_rgb(0, 255, 255)
552
+ AQUAMARINE = HSBK.from_rgb(127, 255, 212)
553
+ AZURE = HSBK.from_rgb(240, 255, 255)
554
+ BEIGE = HSBK.from_rgb(245, 245, 220)
555
+ BISQUE = HSBK.from_rgb(255, 228, 196)
556
+ BLACK = HSBK.from_rgb(0, 0, 0)
557
+ BLANCHED_ALMOND = HSBK.from_rgb(255, 235, 205)
558
+ BLUE = HSBK.from_rgb(0, 0, 255)
559
+ BLUE_VIOLET = HSBK.from_rgb(138, 43, 226)
560
+ BROWN = HSBK.from_rgb(165, 42, 42)
561
+ BURLYWOOD = HSBK.from_rgb(222, 184, 135)
562
+ CADET_BLUE = HSBK.from_rgb(95, 158, 160)
563
+ CHARTREUSE = HSBK.from_rgb(127, 255, 0)
564
+ CHOCOLATE = HSBK.from_rgb(210, 105, 30)
565
+ CORAL = HSBK.from_rgb(255, 127, 80)
566
+ CORNFLOWER_BLUE = HSBK.from_rgb(100, 149, 237)
567
+ CORNSILK = HSBK.from_rgb(255, 248, 220)
568
+ CRIMSON = HSBK.from_rgb(220, 20, 60)
569
+ CYAN = HSBK.from_rgb(0, 255, 255)
570
+ DARK_BLUE = HSBK.from_rgb(0, 0, 139)
571
+ DARK_CYAN = HSBK.from_rgb(0, 139, 139)
572
+ DARK_GOLDENROD = HSBK.from_rgb(184, 134, 11)
573
+ DARK_GRAY = HSBK.from_rgb(169, 169, 169)
574
+ DARK_GREEN = HSBK.from_rgb(0, 100, 0)
575
+ DARK_GREY = HSBK.from_rgb(169, 169, 169)
576
+ DARK_KHAKI = HSBK.from_rgb(189, 183, 107)
577
+ DARK_MAGENTA = HSBK.from_rgb(139, 0, 139)
578
+ DARK_OLIVE_GREEN = HSBK.from_rgb(85, 107, 47)
579
+ DARK_ORANGE = HSBK.from_rgb(255, 140, 0)
580
+ DARK_ORCHID = HSBK.from_rgb(153, 50, 204)
581
+ DARK_RED = HSBK.from_rgb(139, 0, 0)
582
+ DARK_SALMON = HSBK.from_rgb(233, 150, 122)
583
+ DARK_SEA_GREEN = HSBK.from_rgb(143, 188, 143)
584
+ DARK_SLATE_BLUE = HSBK.from_rgb(72, 61, 139)
585
+ DARK_SLATE_GRAY = HSBK.from_rgb(47, 79, 79)
586
+ DARK_SLATE_GREY = HSBK.from_rgb(47, 79, 79)
587
+ DARK_TURQUOISE = HSBK.from_rgb(0, 206, 209)
588
+ DARK_VIOLET = HSBK.from_rgb(148, 0, 211)
589
+ DEEP_PINK = HSBK.from_rgb(255, 20, 147)
590
+ DEEP_SKY_BLUE = HSBK.from_rgb(0, 191, 255)
591
+ DIM_GRAY = HSBK.from_rgb(105, 105, 105)
592
+ DIM_GREY = HSBK.from_rgb(105, 105, 105)
593
+ DODGER_BLUE = HSBK.from_rgb(30, 144, 255)
594
+ FIREBRICK = HSBK.from_rgb(178, 34, 34)
595
+ FLORAL_WHITE = HSBK.from_rgb(255, 250, 240)
596
+ FOREST_GREEN = HSBK.from_rgb(34, 139, 34)
597
+ FUCHSIA = HSBK.from_rgb(255, 0, 255)
598
+ GAINSBORO = HSBK.from_rgb(220, 220, 220)
599
+ GHOST_WHITE = HSBK.from_rgb(248, 248, 255)
600
+ GOLD = HSBK.from_rgb(255, 215, 0)
601
+ GOLDENROD = HSBK.from_rgb(218, 165, 32)
602
+ GRAY = HSBK.from_rgb(128, 128, 128)
603
+ GREEN = HSBK.from_rgb(0, 128, 0)
604
+ GREEN_YELLOW = HSBK.from_rgb(173, 255, 47)
605
+ GREY = HSBK.from_rgb(128, 128, 128)
606
+ HONEYDEW = HSBK.from_rgb(240, 255, 240)
607
+ HOT_PINK = HSBK.from_rgb(255, 105, 180)
608
+ INDIAN_RED = HSBK.from_rgb(205, 92, 92)
609
+ INDIGO = HSBK.from_rgb(75, 0, 130)
610
+ IVORY = HSBK.from_rgb(255, 255, 240)
611
+ KHAKI = HSBK.from_rgb(240, 230, 140)
612
+ LAVENDER = HSBK.from_rgb(230, 230, 250)
613
+ LAVENDER_BLUSH = HSBK.from_rgb(255, 240, 245)
614
+ LAWN_GREEN = HSBK.from_rgb(124, 252, 0)
615
+ LEMON_CHIFFON = HSBK.from_rgb(255, 250, 205)
616
+ LIGHT_BLUE = HSBK.from_rgb(173, 216, 230)
617
+ LIGHT_CORAL = HSBK.from_rgb(240, 128, 128)
618
+ LIGHT_CYAN = HSBK.from_rgb(224, 255, 255)
619
+ LIGHT_GOLDENROD_YELLOW = HSBK.from_rgb(250, 250, 210)
620
+ LIGHT_GRAY = HSBK.from_rgb(211, 211, 211)
621
+ LIGHT_GREEN = HSBK.from_rgb(144, 238, 144)
622
+ LIGHT_GREY = HSBK.from_rgb(211, 211, 211)
623
+ LIGHT_PINK = HSBK.from_rgb(255, 182, 193)
624
+ LIGHT_SALMON = HSBK.from_rgb(255, 160, 122)
625
+ LIGHT_SEA_GREEN = HSBK.from_rgb(32, 178, 170)
626
+ LIGHT_SKY_BLUE = HSBK.from_rgb(135, 206, 250)
627
+ LIGHT_SLATE_GRAY = HSBK.from_rgb(119, 136, 153)
628
+ LIGHT_SLATE_GREY = HSBK.from_rgb(119, 136, 153)
629
+ LIGHT_STEEL_BLUE = HSBK.from_rgb(176, 196, 222)
630
+ LIGHT_YELLOW = HSBK.from_rgb(255, 255, 224)
631
+ LIME = HSBK.from_rgb(0, 255, 0)
632
+ LIME_GREEN = HSBK.from_rgb(50, 205, 50)
633
+ LINEN = HSBK.from_rgb(250, 240, 230)
634
+ MAGENTA = HSBK.from_rgb(255, 0, 255)
635
+ MAROON = HSBK.from_rgb(128, 0, 0)
636
+ MEDIUM_AQUAMARINE = HSBK.from_rgb(102, 205, 170)
637
+ MEDIUM_BLUE = HSBK.from_rgb(0, 0, 205)
638
+ MEDIUM_ORCHID = HSBK.from_rgb(186, 85, 211)
639
+ MEDIUM_PURPLE = HSBK.from_rgb(147, 112, 219)
640
+ MEDIUM_SEA_GREEN = HSBK.from_rgb(60, 179, 113)
641
+ MEDIUM_SLATE_BLUE = HSBK.from_rgb(123, 104, 238)
642
+ MEDIUM_SPRING_GREEN = HSBK.from_rgb(0, 250, 154)
643
+ MEDIUM_TURQUOISE = HSBK.from_rgb(72, 209, 204)
644
+ MEDIUM_VIOLET_RED = HSBK.from_rgb(199, 21, 133)
645
+ MIDNIGHT_BLUE = HSBK.from_rgb(25, 25, 112)
646
+ MINT_CREAM = HSBK.from_rgb(245, 255, 250)
647
+ MISTY_ROSE = HSBK.from_rgb(255, 228, 225)
648
+ MOCCASIN = HSBK.from_rgb(255, 228, 181)
649
+ NAVAJO_WHITE = HSBK.from_rgb(255, 222, 173)
650
+ NAVY = HSBK.from_rgb(0, 0, 128)
651
+ OLD_LACE = HSBK.from_rgb(253, 245, 230)
652
+ OLIVE = HSBK.from_rgb(128, 128, 0)
653
+ OLIVE_DRAB = HSBK.from_rgb(107, 142, 35)
654
+ ORANGE = HSBK.from_rgb(255, 165, 0)
655
+ ORANGE_RED = HSBK.from_rgb(255, 69, 0)
656
+ ORCHID = HSBK.from_rgb(218, 112, 214)
657
+ PALE_GOLDENROD = HSBK.from_rgb(238, 232, 170)
658
+ PALE_GREEN = HSBK.from_rgb(152, 251, 152)
659
+ PALE_TURQUOISE = HSBK.from_rgb(175, 238, 238)
660
+ PALE_VIOLET_RED = HSBK.from_rgb(219, 112, 147)
661
+ PAPAYA_WHIP = HSBK.from_rgb(255, 239, 213)
662
+ PEACH_PUFF = HSBK.from_rgb(255, 218, 185)
663
+ PERU = HSBK.from_rgb(205, 133, 63)
664
+ PINK = HSBK.from_rgb(255, 192, 203)
665
+ PLUM = HSBK.from_rgb(221, 160, 221)
666
+ POWDER_BLUE = HSBK.from_rgb(176, 224, 230)
667
+ PURPLE = HSBK.from_rgb(128, 0, 128)
668
+ REBECCA_PURPLE = HSBK.from_rgb(102, 51, 153)
669
+ RED = HSBK.from_rgb(255, 0, 0)
670
+ ROSY_BROWN = HSBK.from_rgb(188, 143, 143)
671
+ ROYAL_BLUE = HSBK.from_rgb(65, 105, 225)
672
+ SADDLE_BROWN = HSBK.from_rgb(139, 69, 19)
673
+ SALMON = HSBK.from_rgb(250, 128, 114)
674
+ SANDY_BROWN = HSBK.from_rgb(244, 164, 96)
675
+ SEA_GREEN = HSBK.from_rgb(46, 139, 87)
676
+ SEASHELL = HSBK.from_rgb(255, 245, 238)
677
+ SIENNA = HSBK.from_rgb(160, 82, 45)
678
+ SILVER = HSBK.from_rgb(192, 192, 192)
679
+ SKY_BLUE = HSBK.from_rgb(135, 206, 235)
680
+ SLATE_BLUE = HSBK.from_rgb(106, 90, 205)
681
+ SLATE_GRAY = HSBK.from_rgb(112, 128, 144)
682
+ SLATE_GREY = HSBK.from_rgb(112, 128, 144)
683
+ SNOW = HSBK.from_rgb(255, 250, 250)
684
+ SPRING_GREEN = HSBK.from_rgb(0, 255, 127)
685
+ STEEL_BLUE = HSBK.from_rgb(70, 130, 180)
686
+ TAN = HSBK.from_rgb(210, 180, 140)
687
+ TEAL = HSBK.from_rgb(0, 128, 128)
688
+ THISTLE = HSBK.from_rgb(216, 191, 216)
689
+ TOMATO = HSBK.from_rgb(255, 99, 71)
690
+ TURQUOISE = HSBK.from_rgb(64, 224, 208)
691
+ VIOLET = HSBK.from_rgb(238, 130, 238)
692
+ WHEAT = HSBK.from_rgb(245, 222, 179)
693
+ WHITE = HSBK.from_rgb(255, 255, 255)
694
+ WHITE_SMOKE = HSBK.from_rgb(245, 245, 245)
695
+ YELLOW = HSBK.from_rgb(255, 255, 0)
696
+ YELLOW_GREEN = HSBK.from_rgb(154, 205, 50)
697
+
698
+ # White temperature variants (kelvin-based, warmest to coolest)
699
+ CANDLELIGHT = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_CANDLELIGHT)
700
+ SUNSET = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_SUNSET)
701
+ AMBER = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_AMBER)
702
+ ULTRA_WARM = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_ULTRA_WARM)
703
+ INCANDESCENT = HSBK(
704
+ hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_INCANDESCENT
705
+ )
706
+ WARM = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_WARM)
707
+ NEUTRAL_WARM = HSBK(
708
+ hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_NEUTRAL_WARM
709
+ )
710
+ NEUTRAL = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
711
+ COOL = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_COOL)
712
+ COOL_DAYLIGHT = HSBK(
713
+ hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_COOL_DAYLIGHT
714
+ )
715
+ SOFT_DAYLIGHT = HSBK(
716
+ hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_SOFT_DAYLIGHT
717
+ )
718
+ DAYLIGHT = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_DAYLIGHT)
719
+ NOON_DAYLIGHT = HSBK(
720
+ hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_NOON_DAYLIGHT
721
+ )
722
+ BRIGHT_DAYLIGHT = HSBK(
723
+ hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_BRIGHT_DAYLIGHT
724
+ )
725
+ CLOUDY_DAYLIGHT = HSBK(
726
+ hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_CLOUDY_DAYLIGHT
727
+ )
728
+ BLUE_DAYLIGHT = HSBK(
729
+ hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_BLUE_DAYLIGHT
730
+ )
731
+ BLUE_OVERCAST = HSBK(
732
+ hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_BLUE_OVERCAST
733
+ )
734
+ BLUE_ICE = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_BLUE_ICE)
735
+
736
+ # Pastel variants (reduced saturation)
543
737
  PASTEL_RED = HSBK(hue=0, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
544
- PASTEL_ORANGE = HSBK(hue=30, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
738
+ PASTEL_ORANGE = HSBK(hue=39, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
545
739
  PASTEL_YELLOW = HSBK(hue=60, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
546
740
  PASTEL_GREEN = HSBK(hue=120, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
547
741
  PASTEL_CYAN = HSBK(hue=180, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
548
742
  PASTEL_BLUE = HSBK(hue=240, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
549
- PASTEL_PURPLE = HSBK(hue=270, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
743
+ PASTEL_PURPLE = HSBK(hue=300, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
550
744
  PASTEL_MAGENTA = HSBK(
551
745
  hue=300, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL
552
746
  )
553
- PASTEL_PINK = HSBK(hue=330, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
747
+ PASTEL_PINK = HSBK(hue=350, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
748
+
749
+ # Backwards compatibility aliases
750
+ WHITE_WARM = WARM
751
+ WHITE_NEUTRAL = NEUTRAL
752
+ WHITE_COOL = COOL
753
+ WHITE_DAYLIGHT = DAYLIGHT
@@ -9,7 +9,7 @@ import time
9
9
  import uuid
10
10
  from dataclasses import dataclass, field
11
11
  from math import floor, log10
12
- from typing import TYPE_CHECKING, Generic, Self, TypeVar, cast
12
+ from typing import TYPE_CHECKING, Generic, TypeVar, cast
13
13
 
14
14
  from lifx.const import (
15
15
  DEFAULT_MAX_RETRIES,
@@ -26,6 +26,8 @@ from lifx.protocol import packets
26
26
  from lifx.protocol.models import Serial
27
27
 
28
28
  if TYPE_CHECKING:
29
+ from typing_extensions import Self
30
+
29
31
  from lifx.devices import (
30
32
  CeilingLight,
31
33
  HevLight,
@@ -681,12 +683,13 @@ class Device(Generic[StateT]):
681
683
  async def _setup(self) -> None:
682
684
  """Populate device capabilities, state and metadata."""
683
685
  await self._ensure_capabilities()
684
- async with asyncio.TaskGroup() as tg:
685
- tg.create_task(self.get_host_firmware())
686
- tg.create_task(self.get_wifi_firmware())
687
- tg.create_task(self.get_label())
688
- tg.create_task(self.get_location())
689
- tg.create_task(self.get_group())
686
+ await asyncio.gather(
687
+ self.get_host_firmware(),
688
+ self.get_wifi_firmware(),
689
+ self.get_label(),
690
+ self.get_location(),
691
+ self.get_group(),
692
+ )
690
693
 
691
694
  async def get_mac_address(self) -> str:
692
695
  """Calculate and return the MAC address for this device."""
@@ -1669,21 +1672,21 @@ class Device(Generic[StateT]):
1669
1672
 
1670
1673
  # Fetch semi-static and volatile state in parallel
1671
1674
  # get_color returns color, power, and label in one request
1672
- async with asyncio.TaskGroup() as tg:
1673
- label_task = tg.create_task(self.get_label())
1674
- power_task = tg.create_task(self.get_power())
1675
- host_fw_task = tg.create_task(self.get_host_firmware())
1676
- wifi_fw_task = tg.create_task(self.get_wifi_firmware())
1677
- location_task = tg.create_task(self.get_location())
1678
- group_task = tg.create_task(self.get_group())
1679
-
1680
- # Extract results
1681
- label = label_task.result()
1682
- power = power_task.result()
1683
- host_firmware = host_fw_task.result()
1684
- wifi_firmware = wifi_fw_task.result()
1685
- location_info = location_task.result()
1686
- group_info = group_task.result()
1675
+ (
1676
+ label,
1677
+ power,
1678
+ host_firmware,
1679
+ wifi_firmware,
1680
+ location_info,
1681
+ group_info,
1682
+ ) = await asyncio.gather(
1683
+ self.get_label(),
1684
+ self.get_power(),
1685
+ self.get_host_firmware(),
1686
+ self.get_wifi_firmware(),
1687
+ self.get_location(),
1688
+ self.get_group(),
1689
+ )
1687
1690
 
1688
1691
  # Get MAC address (already calculated in get_host_firmware)
1689
1692
  mac_address = await self.get_mac_address()