lifx-async 4.5.0__tar.gz → 4.6.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 (156) hide show
  1. {lifx_async-4.5.0 → lifx_async-4.6.0}/.github/workflows/ci.yml +4 -4
  2. {lifx_async-4.5.0 → lifx_async-4.6.0}/.github/workflows/docs.yml +3 -3
  3. {lifx_async-4.5.0 → lifx_async-4.6.0}/PKG-INFO +1 -1
  4. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/api/devices.md +57 -0
  5. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/changelog.md +16 -0
  6. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/user-guide/advanced-usage.md +6 -0
  7. lifx_async-4.6.0/docs/user-guide/ceiling-lights.md +330 -0
  8. {lifx_async-4.5.0 → lifx_async-4.6.0}/mkdocs.yml +2 -0
  9. {lifx_async-4.5.0 → lifx_async-4.6.0}/pyproject.toml +1 -1
  10. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/__init__.py +4 -0
  11. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/devices/__init__.py +2 -1
  12. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/devices/base.py +17 -3
  13. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/devices/ceiling.py +150 -2
  14. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/devices/matrix.py +1 -4
  15. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_ceiling.py +743 -0
  16. lifx_async-4.6.0/tests/test_devices/test_state_ceiling.py +426 -0
  17. {lifx_async-4.5.0 → lifx_async-4.6.0}/uv.lock +1 -1
  18. {lifx_async-4.5.0 → lifx_async-4.6.0}/.claude/settings.json +0 -0
  19. {lifx_async-4.5.0 → lifx_async-4.6.0}/.github/dependabot.yml +0 -0
  20. {lifx_async-4.5.0 → lifx_async-4.6.0}/.github/labeler.yml +0 -0
  21. {lifx_async-4.5.0 → lifx_async-4.6.0}/.github/workflows/pr-automation.yml +0 -0
  22. {lifx_async-4.5.0 → lifx_async-4.6.0}/.gitignore +0 -0
  23. {lifx_async-4.5.0 → lifx_async-4.6.0}/.pre-commit-config.yaml +0 -0
  24. {lifx_async-4.5.0 → lifx_async-4.6.0}/CLAUDE.md +0 -0
  25. {lifx_async-4.5.0 → lifx_async-4.6.0}/LICENSE +0 -0
  26. {lifx_async-4.5.0 → lifx_async-4.6.0}/README.md +0 -0
  27. {lifx_async-4.5.0 → lifx_async-4.6.0}/context7.json +0 -0
  28. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/api/colors.md +0 -0
  29. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/api/effects.md +0 -0
  30. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/api/exceptions.md +0 -0
  31. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/api/high-level.md +0 -0
  32. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/api/index.md +0 -0
  33. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/api/network.md +0 -0
  34. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/api/protocol.md +0 -0
  35. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/api/themes.md +0 -0
  36. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/architecture/effects-architecture.md +0 -0
  37. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/architecture/overview.md +0 -0
  38. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/faq.md +0 -0
  39. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/getting-started/effects.md +0 -0
  40. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/getting-started/installation.md +0 -0
  41. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/getting-started/quickstart.md +0 -0
  42. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/getting-started/themes.md +0 -0
  43. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/index.md +0 -0
  44. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/migration/effect-api-changes.md +0 -0
  45. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/stylesheets/extra.css +0 -0
  46. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/user-guide/effects-custom.md +0 -0
  47. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/user-guide/effects-troubleshooting.md +0 -0
  48. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/user-guide/protocol-deep-dive.md +0 -0
  49. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/user-guide/themes.md +0 -0
  50. {lifx_async-4.5.0 → lifx_async-4.6.0}/docs/user-guide/troubleshooting.md +0 -0
  51. {lifx_async-4.5.0 → lifx_async-4.6.0}/examples/01_simple_discovery.py +0 -0
  52. {lifx_async-4.5.0 → lifx_async-4.6.0}/examples/02_simple_control.py +0 -0
  53. {lifx_async-4.5.0 → lifx_async-4.6.0}/examples/03_waveforms.py +0 -0
  54. {lifx_async-4.5.0 → lifx_async-4.6.0}/examples/04_logging.py +0 -0
  55. {lifx_async-4.5.0 → lifx_async-4.6.0}/examples/06_pulse_effect.py +0 -0
  56. {lifx_async-4.5.0 → lifx_async-4.6.0}/examples/07_colorloop_effect.py +0 -0
  57. {lifx_async-4.5.0 → lifx_async-4.6.0}/examples/08_custom_effect.py +0 -0
  58. {lifx_async-4.5.0 → lifx_async-4.6.0}/examples/09_background_effect.py +0 -0
  59. {lifx_async-4.5.0 → lifx_async-4.6.0}/examples/10_find_specific_devices.py +0 -0
  60. {lifx_async-4.5.0 → lifx_async-4.6.0}/examples/11_matrix_basic.py +0 -0
  61. {lifx_async-4.5.0 → lifx_async-4.6.0}/examples/12_matrix_effects.py +0 -0
  62. {lifx_async-4.5.0 → lifx_async-4.6.0}/examples/13_matrix_large.py +0 -0
  63. {lifx_async-4.5.0 → lifx_async-4.6.0}/renovate.json +0 -0
  64. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/api.py +0 -0
  65. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/color.py +0 -0
  66. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/const.py +0 -0
  67. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/devices/hev.py +0 -0
  68. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/devices/infrared.py +0 -0
  69. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/devices/light.py +0 -0
  70. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/devices/multizone.py +0 -0
  71. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/effects/__init__.py +0 -0
  72. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/effects/base.py +0 -0
  73. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/effects/colorloop.py +0 -0
  74. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/effects/conductor.py +0 -0
  75. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/effects/const.py +0 -0
  76. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/effects/models.py +0 -0
  77. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/effects/pulse.py +0 -0
  78. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/effects/state_manager.py +0 -0
  79. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/exceptions.py +0 -0
  80. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/network/__init__.py +0 -0
  81. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/network/connection.py +0 -0
  82. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/network/discovery.py +0 -0
  83. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/network/message.py +0 -0
  84. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/network/transport.py +0 -0
  85. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/products/__init__.py +0 -0
  86. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/products/generator.py +0 -0
  87. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/products/quirks.py +0 -0
  88. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/products/registry.py +0 -0
  89. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/protocol/__init__.py +0 -0
  90. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/protocol/base.py +0 -0
  91. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/protocol/generator.py +0 -0
  92. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/protocol/header.py +0 -0
  93. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/protocol/models.py +0 -0
  94. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/protocol/packets.py +0 -0
  95. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/protocol/protocol_types.py +0 -0
  96. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/protocol/serializer.py +0 -0
  97. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/py.typed +0 -0
  98. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/theme/__init__.py +0 -0
  99. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/theme/canvas.py +0 -0
  100. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/theme/generators.py +0 -0
  101. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/theme/library.py +0 -0
  102. {lifx_async-4.5.0 → lifx_async-4.6.0}/src/lifx/theme/theme.py +0 -0
  103. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/__init__.py +0 -0
  104. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/conftest.py +0 -0
  105. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_api/__init__.py +0 -0
  106. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_api/test_api_apply_theme.py +0 -0
  107. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_api/test_api_batch_errors.py +0 -0
  108. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_api/test_api_batch_operations.py +0 -0
  109. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_api/test_api_discovery.py +0 -0
  110. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_api/test_api_organization.py +0 -0
  111. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_color.py +0 -0
  112. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/__init__.py +0 -0
  113. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/conftest.py +0 -0
  114. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_base.py +0 -0
  115. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_hev.py +0 -0
  116. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_infrared.py +0 -0
  117. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_light.py +0 -0
  118. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_mac_address.py +0 -0
  119. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_matrix.py +0 -0
  120. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_multizone.py +0 -0
  121. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_state_hev.py +0 -0
  122. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_state_infrared.py +0 -0
  123. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_state_light.py +0 -0
  124. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_state_management.py +0 -0
  125. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_state_matrix.py +0 -0
  126. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_devices/test_state_multizone.py +0 -0
  127. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_effects/__init__.py +0 -0
  128. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_effects/test_base.py +0 -0
  129. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_effects/test_capability_filtering.py +0 -0
  130. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_effects/test_colorloop.py +0 -0
  131. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_effects/test_integration.py +0 -0
  132. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_effects/test_models.py +0 -0
  133. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_effects/test_pulse.py +0 -0
  134. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_effects/test_state_manager.py +0 -0
  135. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_network/__init__.py +0 -0
  136. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_network/test_concurrent_requests.py +0 -0
  137. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_network/test_connection.py +0 -0
  138. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_network/test_discovery_devices.py +0 -0
  139. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_network/test_discovery_errors.py +0 -0
  140. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_network/test_message.py +0 -0
  141. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_network/test_message_advanced.py +0 -0
  142. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_network/test_transport.py +0 -0
  143. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_products/test_product_generator.py +0 -0
  144. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_products/test_registry.py +0 -0
  145. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_protocol/test_generated.py +0 -0
  146. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_protocol/test_header.py +0 -0
  147. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_protocol/test_protocol_generator.py +0 -0
  148. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_protocol/test_serializer.py +0 -0
  149. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_theme/__init__.py +0 -0
  150. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_theme/conftest.py +0 -0
  151. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_theme/test_apply_theme.py +0 -0
  152. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_theme/test_canvas.py +0 -0
  153. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_theme/test_generators.py +0 -0
  154. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_theme/test_library.py +0 -0
  155. {lifx_async-4.5.0 → lifx_async-4.6.0}/tests/test_theme/test_theme.py +0 -0
  156. {lifx_async-4.5.0 → lifx_async-4.6.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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
43
+ uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # 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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
85
+ uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # 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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
137
+ uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # 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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
201
+ uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # 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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
38
+ uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # 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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
72
+ uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # 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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
100
+ uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # 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.5.0
3
+ Version: 4.6.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>
@@ -81,6 +81,32 @@ The `MatrixLight` class controls LIFX matrix devices (tiles, candle, path) with
81
81
  filters:
82
82
  - "!^_"
83
83
 
84
+ ## Ceiling Light
85
+
86
+ The `CeilingLight` class extends `MatrixLight` with independent control over uplight and downlight components for LIFX Ceiling fixtures.
87
+
88
+ ::: lifx.devices.ceiling.CeilingLight
89
+ options:
90
+ show_root_heading: true
91
+ heading_level: 3
92
+ members_order: source
93
+ show_if_no_docstring: false
94
+ filters:
95
+ - "!^_"
96
+
97
+ ### CeilingLightState
98
+
99
+ The `CeilingLightState` dataclass extends `MatrixLightState` with ceiling-specific component information. It is returned by `CeilingLight.state` after connecting to a device.
100
+
101
+ ::: lifx.devices.ceiling.CeilingLightState
102
+ options:
103
+ show_root_heading: true
104
+ heading_level: 4
105
+ members_order: source
106
+ show_if_no_docstring: false
107
+ filters:
108
+ - "!^_"
109
+
84
110
  ## Device Properties
85
111
 
86
112
  ### MAC Address
@@ -268,3 +294,34 @@ async def main():
268
294
  # Stop the effect
269
295
  await light.set_effect(effect_type=FirmwareEffect.OFF)
270
296
  ```
297
+
298
+ ### Ceiling Light Control
299
+
300
+ ```python
301
+ from lifx import CeilingLight, HSBK
302
+
303
+
304
+ async def main():
305
+ async with await CeilingLight.from_ip("192.168.1.100") as ceiling:
306
+ # Set downlight to warm white
307
+ await ceiling.set_downlight_colors(
308
+ HSBK(hue=0, saturation=0, brightness=0.8, kelvin=3000)
309
+ )
310
+
311
+ # Set uplight to a dim ambient glow
312
+ await ceiling.set_uplight_color(
313
+ HSBK(hue=30, saturation=0.2, brightness=0.3, kelvin=2700)
314
+ )
315
+
316
+ # Turn uplight off (stores color for later restoration)
317
+ await ceiling.turn_uplight_off()
318
+
319
+ # Turn uplight back on (restores previous color)
320
+ await ceiling.turn_uplight_on()
321
+
322
+ # Check component state
323
+ if ceiling.downlight_is_on:
324
+ print("Downlight is currently on")
325
+ ```
326
+
327
+ For detailed CeilingLight usage, see the [Ceiling Lights User Guide](../user-guide/ceiling-lights.md).
@@ -2,6 +2,22 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v4.6.0 (2025-12-11)
6
+
7
+ ### Features
8
+
9
+ - **devices**: Add CeilingLightState dataclass for ceiling component state
10
+ ([`607f15c`](https://github.com/Djelibeybi/lifx-async/commit/607f15c3ed3508a883523ecee940959806d49400))
11
+
12
+
13
+ ## v4.5.1 (2025-12-11)
14
+
15
+ ### Bug Fixes
16
+
17
+ - **devices**: Export CeilingLight add add user guide and API documentation
18
+ ([`10e0089`](https://github.com/Djelibeybi/lifx-async/commit/10e008983ffd8b233dd2427a4a4f64661c8f14bd))
19
+
20
+
5
21
  ## v4.5.0 (2025-12-08)
6
22
 
7
23
  ### Features
@@ -119,6 +119,12 @@ async def use_cached_or_fetch():
119
119
  - `MatrixLight.device_chain` - Details of each tile on the chain
120
120
  - `MatrixLight.tile_effect` - Either MORPH, FLAME, SKY or OFF
121
121
 
122
+ #### CeilingLight properties:
123
+
124
+ - `CeilingLight.uplight_zone` - Zone index of the uplight component
125
+ - `CeilingLight.downlight_zones` - Slice representing downlight zones
126
+ - `CeilingLight.uplight_is_on` - True if uplight has brightness > 0 (requires recent data)
127
+ - `CeilingLight.downlight_is_on` - True if any downlight zone has brightness > 0 (requires recent data)
122
128
 
123
129
  **Note**: Volatile state properties (power, color, hev_cycle, zones, tile_colors) have been removed. Always use `get_*()` methods to fetch these values from devices as they change too frequently to benefit from caching.
124
130
 
@@ -0,0 +1,330 @@
1
+ # Ceiling Lights
2
+
3
+ LIFX Ceiling lights are unique fixtures that combine two lighting components in one device:
4
+
5
+ - **Downlight**: Main illumination with multiple addressable zones (63 or 127 zones)
6
+ - **Uplight**: Ambient/indirect lighting via a single zone
7
+
8
+ The `CeilingLight` class provides high-level control over these components while inheriting full matrix functionality from `MatrixLight`.
9
+
10
+ ## Supported Devices
11
+
12
+ | Product | Zones | Layout |
13
+ |---------|-------|--------|
14
+ | LIFX Ceiling (US/Intl) | 64 | 8x8 grid, zone 63 = uplight |
15
+ | LIFX Ceiling Capsule (US/Intl) | 128 | 16x8 grid, zone 127 = uplight |
16
+
17
+ ## Quick Start
18
+
19
+ ```python
20
+ from lifx import CeilingLight
21
+ from lifx.color import HSBK
22
+
23
+ async def main():
24
+ async with await CeilingLight.from_ip("192.168.1.100") as ceiling:
25
+ # Set downlight to warm white
26
+ await ceiling.set_downlight_colors(
27
+ HSBK(hue=0, saturation=0, brightness=1.0, kelvin=3000)
28
+ )
29
+
30
+ # Set uplight to a dim, warm ambient glow
31
+ await ceiling.set_uplight_color(
32
+ HSBK(hue=30, saturation=0.2, brightness=0.3, kelvin=2700)
33
+ )
34
+ ```
35
+
36
+ ## Component Control
37
+
38
+ ### Setting Colors
39
+
40
+ #### Downlight
41
+
42
+ Set all downlight zones to the same color:
43
+
44
+ ```python
45
+ # Single color for all zones
46
+ await ceiling.set_downlight_colors(
47
+ HSBK(hue=0, saturation=0, brightness=0.8, kelvin=4000)
48
+ )
49
+ ```
50
+
51
+ Or set each zone individually:
52
+
53
+ ```python
54
+ # Create a gradient across all zones
55
+ zone_count = len(range(*ceiling.downlight_zones.indices(256)))
56
+ colors = [
57
+ HSBK(hue=(i * 360 / zone_count), saturation=1.0, brightness=0.5, kelvin=3500)
58
+ for i in range(zone_count)
59
+ ]
60
+ await ceiling.set_downlight_colors(colors)
61
+ ```
62
+
63
+ #### Uplight
64
+
65
+ ```python
66
+ await ceiling.set_uplight_color(
67
+ HSBK(hue=30, saturation=0.1, brightness=0.4, kelvin=2700)
68
+ )
69
+ ```
70
+
71
+ ### Reading Current Colors
72
+
73
+ ```python
74
+ # Get current uplight color
75
+ uplight_color = await ceiling.get_uplight_color()
76
+ print(f"Uplight: H={uplight_color.hue}, B={uplight_color.brightness}")
77
+
78
+ # Get all downlight colors
79
+ downlight_colors = await ceiling.get_downlight_colors()
80
+ print(f"Downlight zones: {len(downlight_colors)}")
81
+ ```
82
+
83
+ ### Turning Components On/Off
84
+
85
+ The `turn_*_on()` and `turn_*_off()` methods provide smart state management:
86
+
87
+ ```python
88
+ # Turn off uplight (stores current color for later restoration)
89
+ await ceiling.turn_uplight_off()
90
+
91
+ # Turn uplight back on (restores previous color)
92
+ await ceiling.turn_uplight_on()
93
+
94
+ # Turn on with a specific color
95
+ await ceiling.turn_uplight_on(
96
+ color=HSBK(hue=0, saturation=0, brightness=1.0, kelvin=3500)
97
+ )
98
+ ```
99
+
100
+ The same pattern works for downlights:
101
+
102
+ ```python
103
+ # Turn off downlight
104
+ await ceiling.turn_downlight_off()
105
+
106
+ # Turn downlight back on
107
+ await ceiling.turn_downlight_on()
108
+
109
+ # Turn on with specific colors
110
+ await ceiling.turn_downlight_on(
111
+ colors=HSBK(hue=0, saturation=0, brightness=0.8, kelvin=4000)
112
+ )
113
+ ```
114
+
115
+ ### Checking Component State
116
+
117
+ ```python
118
+ # Check if components are on
119
+ if ceiling.uplight_is_on:
120
+ print("Uplight is on")
121
+
122
+ if ceiling.downlight_is_on:
123
+ print("Downlight is on")
124
+ ```
125
+
126
+ !!! note "State Properties Require Recent Data"
127
+ The `uplight_is_on` and `downlight_is_on` properties rely on cached data.
128
+ Call `get_uplight_color()` or `get_downlight_colors()` first to ensure
129
+ accurate state.
130
+
131
+ ## Device State
132
+
133
+ After connecting to a CeilingLight, you can access the complete device state via the `state` property, which returns a `CeilingLightState` dataclass:
134
+
135
+ ```python
136
+ from lifx import CeilingLight, CeilingLightState
137
+
138
+ async with await CeilingLight.from_ip("192.168.1.100") as ceiling:
139
+ state: CeilingLightState = ceiling.state
140
+
141
+ # Access ceiling-specific state
142
+ print(f"Uplight color: {state.uplight_color}")
143
+ print(f"Uplight is on: {state.uplight_is_on}")
144
+ print(f"Downlight zones: {len(state.downlight_colors)}")
145
+ print(f"Downlight is on: {state.downlight_is_on}")
146
+
147
+ # Access inherited state from MatrixLightState/LightState
148
+ print(f"Device label: {state.label}")
149
+ print(f"Power: {'on' if state.power else 'off'}")
150
+ print(f"Model: {state.model}")
151
+ ```
152
+
153
+ ### CeilingLightState Attributes
154
+
155
+ `CeilingLightState` extends `MatrixLightState` with ceiling-specific attributes:
156
+
157
+ | Attribute | Type | Description |
158
+ |-----------|------|-------------|
159
+ | `uplight_color` | `HSBK` | Current color of the uplight component |
160
+ | `downlight_colors` | `list[HSBK]` | Colors for each downlight zone (63 or 127) |
161
+ | `uplight_is_on` | `bool` | True if uplight brightness > 0 |
162
+ | `downlight_is_on` | `bool` | True if any downlight zone brightness > 0 |
163
+ | `uplight_zone` | `int` | Zone index for uplight (63 or 127) |
164
+ | `downlight_zones` | `slice` | Slice for downlight zones |
165
+
166
+ Plus all attributes inherited from `MatrixLightState`: `chain`, `tile_colors`, `tile_count`, `effect`, and from `LightState`: `color`, `power`, `label`, `model`, `serial`, `mac_address`, `capabilities`, etc.
167
+
168
+ ## Zone Layout
169
+
170
+ Access the component zone indices directly:
171
+
172
+ ```python
173
+ async with await CeilingLight.from_ip("192.168.1.100") as ceiling:
174
+ # Get uplight zone index (63 or 127 depending on model)
175
+ uplight_idx = ceiling.uplight_zone
176
+ print(f"Uplight zone: {uplight_idx}")
177
+
178
+ # Get downlight zones as a slice
179
+ downlight_slice = ceiling.downlight_zones
180
+ print(f"Downlight zones: {downlight_slice}") # slice(0, 63) or slice(0, 127)
181
+
182
+ # Calculate number of downlight zones
183
+ zone_count = len(range(*downlight_slice.indices(256)))
184
+ print(f"Number of downlight zones: {zone_count}")
185
+ ```
186
+
187
+ ## State Persistence
188
+
189
+ CeilingLight supports optional state persistence to preserve component colors across sessions:
190
+
191
+ ```python
192
+ async with await CeilingLight.from_ip(
193
+ "192.168.1.100",
194
+ state_file="~/.lifx/ceiling_state.json"
195
+ ) as ceiling:
196
+ # Colors are automatically loaded from file on connection
197
+ # and saved when using turn_*_off() methods
198
+
199
+ await ceiling.turn_uplight_off() # Saves current color to file
200
+ # ... later ...
201
+ await ceiling.turn_uplight_on() # Restores from file if available
202
+ ```
203
+
204
+ The state file stores colors per device serial number, supporting multiple devices:
205
+
206
+ ```json
207
+ {
208
+ "d073d5123456": {
209
+ "uplight": {
210
+ "hue": 30.0,
211
+ "saturation": 0.2,
212
+ "brightness": 0.4,
213
+ "kelvin": 2700
214
+ },
215
+ "downlight": [
216
+ {"hue": 0.0, "saturation": 0.0, "brightness": 0.8, "kelvin": 4000}
217
+ ]
218
+ }
219
+ }
220
+ ```
221
+
222
+ ## Brightness Determination
223
+
224
+ When calling `turn_uplight_on()` or `turn_downlight_on()` without a color parameter, CeilingLight uses the following priority to determine brightness:
225
+
226
+ 1. **Stored state**: If a color was previously saved (via `turn_*_off()` or `set_*_color()`)
227
+ 2. **Infer from other component**: Average brightness of the other component
228
+ 3. **Default**: 80% brightness
229
+
230
+ This ensures a reasonable brightness level even when no state is available.
231
+
232
+ ## Transition Duration
233
+
234
+ All color-setting methods support smooth transitions:
235
+
236
+ ```python
237
+ # 2-second transition to new color
238
+ await ceiling.set_uplight_color(
239
+ HSBK(hue=0, saturation=0, brightness=1.0, kelvin=3500),
240
+ duration=2.0 # seconds
241
+ )
242
+
243
+ # Instant change (default)
244
+ await ceiling.set_downlight_colors(
245
+ HSBK(hue=240, saturation=1.0, brightness=0.5, kelvin=3500),
246
+ duration=0.0
247
+ )
248
+ ```
249
+
250
+ ## MatrixLight Compatibility
251
+
252
+ CeilingLight extends `MatrixLight`, so all matrix operations are available:
253
+
254
+ ```python
255
+ async with await CeilingLight.from_ip("192.168.1.100") as ceiling:
256
+ # Use MatrixLight methods directly
257
+ all_colors = await ceiling.get_all_tile_colors()
258
+ tile_chain = await ceiling.get_tile_chain()
259
+
260
+ # Set raw matrix colors (bypasses component abstraction)
261
+ await ceiling.set_matrix_colors(0, colors)
262
+
263
+ # Apply effects
264
+ from lifx.protocol.protocol_types import TileEffectType
265
+ await ceiling.set_tile_effect(
266
+ effect_type=TileEffectType.MORPH,
267
+ speed=5000,
268
+ )
269
+ ```
270
+
271
+ ## Example: Night Mode
272
+
273
+ Create a subtle night light with dim uplight and downlight off:
274
+
275
+ ```python
276
+ from lifx import CeilingLight
277
+ from lifx.color import HSBK
278
+
279
+ async def night_mode(ip: str):
280
+ async with await CeilingLight.from_ip(ip) as ceiling:
281
+ # Store current colors before turning off
282
+ await ceiling.turn_downlight_off()
283
+
284
+ # Set uplight to very dim warm glow
285
+ await ceiling.set_uplight_color(
286
+ HSBK(hue=30, saturation=0.3, brightness=0.05, kelvin=2200),
287
+ duration=2.0
288
+ )
289
+ ```
290
+
291
+ ## Example: Daytime Productivity
292
+
293
+ Bright, cool white for focus:
294
+
295
+ ```python
296
+ async def daytime_mode(ip: str):
297
+ async with await CeilingLight.from_ip(ip) as ceiling:
298
+ # Bright cool downlight for task lighting
299
+ await ceiling.set_downlight_colors(
300
+ HSBK(hue=0, saturation=0, brightness=1.0, kelvin=5500),
301
+ duration=1.0
302
+ )
303
+
304
+ # Turn off uplight during the day
305
+ await ceiling.turn_uplight_off(duration=1.0)
306
+ ```
307
+
308
+ ## Example: Evening Ambiance
309
+
310
+ Warm tones with accent uplight:
311
+
312
+ ```python
313
+ async def evening_mode(ip: str):
314
+ async with await CeilingLight.from_ip(ip) as ceiling:
315
+ # Dimmed warm downlight
316
+ await ceiling.set_downlight_colors(
317
+ HSBK(hue=30, saturation=0.1, brightness=0.4, kelvin=2700),
318
+ duration=2.0
319
+ )
320
+
321
+ # Colorful uplight accent
322
+ await ceiling.set_uplight_color(
323
+ HSBK(hue=280, saturation=0.6, brightness=0.3, kelvin=3500),
324
+ duration=2.0
325
+ )
326
+ ```
327
+
328
+ ## API Reference
329
+
330
+ See [CeilingLight API Reference](../api/devices.md#ceiling-light) for complete method documentation.
@@ -130,6 +130,7 @@ plugins:
130
130
  - getting-started/effects.md: Light effects and animations
131
131
  User Guide:
132
132
  - user-guide/themes.md: Working with color themes
133
+ - user-guide/ceiling-lights.md: Controlling LIFX Ceiling uplight and downlight components
133
134
  - user-guide/advanced-usage.md: Advanced usage patterns and best practices
134
135
  - user-guide/effects-custom.md: Creating custom light effects
135
136
  - user-guide/troubleshooting.md: Common issues and solutions
@@ -199,6 +200,7 @@ nav:
199
200
  - Light Effects: getting-started/effects.md
200
201
  - User Guide:
201
202
  - Themes: user-guide/themes.md
203
+ - Ceiling Lights: user-guide/ceiling-lights.md
202
204
  - Advanced Usage: user-guide/advanced-usage.md
203
205
  - Custom Effects: user-guide/effects-custom.md
204
206
  - Troubleshooting: user-guide/troubleshooting.md
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lifx-async"
3
- version = "4.5.0"
3
+ version = "4.6.0"
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"
@@ -16,6 +16,8 @@ from lifx.api import (
16
16
  )
17
17
  from lifx.color import HSBK, Colors
18
18
  from lifx.devices import (
19
+ CeilingLight,
20
+ CeilingLightState,
19
21
  Device,
20
22
  DeviceInfo,
21
23
  DeviceVersion,
@@ -71,6 +73,8 @@ __all__ = [
71
73
  "MultiZoneLightState",
72
74
  "MatrixLight",
73
75
  "MatrixLightState",
76
+ "CeilingLight",
77
+ "CeilingLightState",
74
78
  # Color
75
79
  "HSBK",
76
80
  "Colors",
@@ -10,7 +10,7 @@ from lifx.devices.base import (
10
10
  FirmwareInfo,
11
11
  WifiInfo,
12
12
  )
13
- from lifx.devices.ceiling import CeilingLight
13
+ from lifx.devices.ceiling import CeilingLight, CeilingLightState
14
14
  from lifx.devices.hev import HevLight, HevLightState
15
15
  from lifx.devices.infrared import InfraredLight, InfraredLightState
16
16
  from lifx.devices.light import Light, LightState
@@ -19,6 +19,7 @@ from lifx.devices.multizone import MultiZoneEffect, MultiZoneLight, MultiZoneLig
19
19
 
20
20
  __all__ = [
21
21
  "CeilingLight",
22
+ "CeilingLightState",
22
23
  "CollectionInfo",
23
24
  "Device",
24
25
  "DeviceInfo",
@@ -26,7 +26,14 @@ from lifx.protocol import packets
26
26
  from lifx.protocol.models import Serial
27
27
 
28
28
  if TYPE_CHECKING:
29
- from lifx.devices import HevLight, InfraredLight, Light, MatrixLight, MultiZoneLight
29
+ from lifx.devices import (
30
+ CeilingLight,
31
+ HevLight,
32
+ InfraredLight,
33
+ Light,
34
+ MatrixLight,
35
+ MultiZoneLight,
36
+ )
30
37
 
31
38
  _LOGGER = logging.getLogger(__name__)
32
39
 
@@ -509,7 +516,7 @@ class Device(Generic[StateT]):
509
516
  port: int = LIFX_UDP_PORT,
510
517
  timeout: float = DEFAULT_REQUEST_TIMEOUT,
511
518
  max_retries: int = DEFAULT_MAX_RETRIES,
512
- ) -> Light | HevLight | InfraredLight | MultiZoneLight | MatrixLight:
519
+ ) -> Light | HevLight | InfraredLight | MultiZoneLight | MatrixLight | CeilingLight:
513
520
  """Create and return a fully initialized device instance.
514
521
 
515
522
  This factory method creates the appropriate device type (Light, etc)
@@ -610,7 +617,14 @@ class Device(Generic[StateT]):
610
617
 
611
618
  device_class: type[Device] = cls
612
619
 
613
- if product_info.has_matrix:
620
+ # Check for ceiling products first (subset of matrix devices)
621
+ from lifx.products import is_ceiling_product
622
+
623
+ if is_ceiling_product(version.product):
624
+ from lifx.devices.ceiling import CeilingLight
625
+
626
+ device_class = CeilingLight
627
+ elif product_info.has_matrix:
614
628
  from lifx.devices.matrix import MatrixLight
615
629
 
616
630
  device_class = MatrixLight