lifx-async 4.5.0__tar.gz → 4.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. {lifx_async-4.5.0 → lifx_async-4.5.1}/.github/workflows/ci.yml +4 -4
  2. {lifx_async-4.5.0 → lifx_async-4.5.1}/.github/workflows/docs.yml +3 -3
  3. {lifx_async-4.5.0 → lifx_async-4.5.1}/PKG-INFO +1 -1
  4. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/api/devices.md +44 -0
  5. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/changelog.md +8 -0
  6. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/user-guide/advanced-usage.md +6 -0
  7. lifx_async-4.5.1/docs/user-guide/ceiling-lights.md +293 -0
  8. {lifx_async-4.5.0 → lifx_async-4.5.1}/mkdocs.yml +2 -0
  9. {lifx_async-4.5.0 → lifx_async-4.5.1}/pyproject.toml +1 -1
  10. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/__init__.py +2 -0
  11. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/devices/matrix.py +1 -4
  12. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_ceiling.py +743 -0
  13. {lifx_async-4.5.0 → lifx_async-4.5.1}/uv.lock +1 -1
  14. {lifx_async-4.5.0 → lifx_async-4.5.1}/.claude/settings.json +0 -0
  15. {lifx_async-4.5.0 → lifx_async-4.5.1}/.github/dependabot.yml +0 -0
  16. {lifx_async-4.5.0 → lifx_async-4.5.1}/.github/labeler.yml +0 -0
  17. {lifx_async-4.5.0 → lifx_async-4.5.1}/.github/workflows/pr-automation.yml +0 -0
  18. {lifx_async-4.5.0 → lifx_async-4.5.1}/.gitignore +0 -0
  19. {lifx_async-4.5.0 → lifx_async-4.5.1}/.pre-commit-config.yaml +0 -0
  20. {lifx_async-4.5.0 → lifx_async-4.5.1}/CLAUDE.md +0 -0
  21. {lifx_async-4.5.0 → lifx_async-4.5.1}/LICENSE +0 -0
  22. {lifx_async-4.5.0 → lifx_async-4.5.1}/README.md +0 -0
  23. {lifx_async-4.5.0 → lifx_async-4.5.1}/context7.json +0 -0
  24. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/api/colors.md +0 -0
  25. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/api/effects.md +0 -0
  26. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/api/exceptions.md +0 -0
  27. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/api/high-level.md +0 -0
  28. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/api/index.md +0 -0
  29. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/api/network.md +0 -0
  30. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/api/protocol.md +0 -0
  31. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/api/themes.md +0 -0
  32. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/architecture/effects-architecture.md +0 -0
  33. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/architecture/overview.md +0 -0
  34. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/faq.md +0 -0
  35. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/getting-started/effects.md +0 -0
  36. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/getting-started/installation.md +0 -0
  37. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/getting-started/quickstart.md +0 -0
  38. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/getting-started/themes.md +0 -0
  39. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/index.md +0 -0
  40. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/migration/effect-api-changes.md +0 -0
  41. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/stylesheets/extra.css +0 -0
  42. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/user-guide/effects-custom.md +0 -0
  43. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/user-guide/effects-troubleshooting.md +0 -0
  44. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/user-guide/protocol-deep-dive.md +0 -0
  45. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/user-guide/themes.md +0 -0
  46. {lifx_async-4.5.0 → lifx_async-4.5.1}/docs/user-guide/troubleshooting.md +0 -0
  47. {lifx_async-4.5.0 → lifx_async-4.5.1}/examples/01_simple_discovery.py +0 -0
  48. {lifx_async-4.5.0 → lifx_async-4.5.1}/examples/02_simple_control.py +0 -0
  49. {lifx_async-4.5.0 → lifx_async-4.5.1}/examples/03_waveforms.py +0 -0
  50. {lifx_async-4.5.0 → lifx_async-4.5.1}/examples/04_logging.py +0 -0
  51. {lifx_async-4.5.0 → lifx_async-4.5.1}/examples/06_pulse_effect.py +0 -0
  52. {lifx_async-4.5.0 → lifx_async-4.5.1}/examples/07_colorloop_effect.py +0 -0
  53. {lifx_async-4.5.0 → lifx_async-4.5.1}/examples/08_custom_effect.py +0 -0
  54. {lifx_async-4.5.0 → lifx_async-4.5.1}/examples/09_background_effect.py +0 -0
  55. {lifx_async-4.5.0 → lifx_async-4.5.1}/examples/10_find_specific_devices.py +0 -0
  56. {lifx_async-4.5.0 → lifx_async-4.5.1}/examples/11_matrix_basic.py +0 -0
  57. {lifx_async-4.5.0 → lifx_async-4.5.1}/examples/12_matrix_effects.py +0 -0
  58. {lifx_async-4.5.0 → lifx_async-4.5.1}/examples/13_matrix_large.py +0 -0
  59. {lifx_async-4.5.0 → lifx_async-4.5.1}/renovate.json +0 -0
  60. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/api.py +0 -0
  61. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/color.py +0 -0
  62. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/const.py +0 -0
  63. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/devices/__init__.py +0 -0
  64. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/devices/base.py +0 -0
  65. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/devices/ceiling.py +0 -0
  66. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/devices/hev.py +0 -0
  67. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/devices/infrared.py +0 -0
  68. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/devices/light.py +0 -0
  69. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/devices/multizone.py +0 -0
  70. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/effects/__init__.py +0 -0
  71. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/effects/base.py +0 -0
  72. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/effects/colorloop.py +0 -0
  73. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/effects/conductor.py +0 -0
  74. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/effects/const.py +0 -0
  75. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/effects/models.py +0 -0
  76. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/effects/pulse.py +0 -0
  77. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/effects/state_manager.py +0 -0
  78. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/exceptions.py +0 -0
  79. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/network/__init__.py +0 -0
  80. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/network/connection.py +0 -0
  81. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/network/discovery.py +0 -0
  82. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/network/message.py +0 -0
  83. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/network/transport.py +0 -0
  84. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/products/__init__.py +0 -0
  85. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/products/generator.py +0 -0
  86. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/products/quirks.py +0 -0
  87. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/products/registry.py +0 -0
  88. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/protocol/__init__.py +0 -0
  89. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/protocol/base.py +0 -0
  90. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/protocol/generator.py +0 -0
  91. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/protocol/header.py +0 -0
  92. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/protocol/models.py +0 -0
  93. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/protocol/packets.py +0 -0
  94. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/protocol/protocol_types.py +0 -0
  95. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/protocol/serializer.py +0 -0
  96. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/py.typed +0 -0
  97. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/theme/__init__.py +0 -0
  98. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/theme/canvas.py +0 -0
  99. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/theme/generators.py +0 -0
  100. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/theme/library.py +0 -0
  101. {lifx_async-4.5.0 → lifx_async-4.5.1}/src/lifx/theme/theme.py +0 -0
  102. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/__init__.py +0 -0
  103. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/conftest.py +0 -0
  104. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_api/__init__.py +0 -0
  105. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_api/test_api_apply_theme.py +0 -0
  106. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_api/test_api_batch_errors.py +0 -0
  107. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_api/test_api_batch_operations.py +0 -0
  108. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_api/test_api_discovery.py +0 -0
  109. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_api/test_api_organization.py +0 -0
  110. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_color.py +0 -0
  111. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/__init__.py +0 -0
  112. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/conftest.py +0 -0
  113. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_base.py +0 -0
  114. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_hev.py +0 -0
  115. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_infrared.py +0 -0
  116. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_light.py +0 -0
  117. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_mac_address.py +0 -0
  118. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_matrix.py +0 -0
  119. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_multizone.py +0 -0
  120. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_state_hev.py +0 -0
  121. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_state_infrared.py +0 -0
  122. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_state_light.py +0 -0
  123. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_state_management.py +0 -0
  124. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_state_matrix.py +0 -0
  125. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_devices/test_state_multizone.py +0 -0
  126. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_effects/__init__.py +0 -0
  127. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_effects/test_base.py +0 -0
  128. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_effects/test_capability_filtering.py +0 -0
  129. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_effects/test_colorloop.py +0 -0
  130. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_effects/test_integration.py +0 -0
  131. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_effects/test_models.py +0 -0
  132. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_effects/test_pulse.py +0 -0
  133. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_effects/test_state_manager.py +0 -0
  134. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_network/__init__.py +0 -0
  135. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_network/test_concurrent_requests.py +0 -0
  136. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_network/test_connection.py +0 -0
  137. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_network/test_discovery_devices.py +0 -0
  138. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_network/test_discovery_errors.py +0 -0
  139. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_network/test_message.py +0 -0
  140. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_network/test_message_advanced.py +0 -0
  141. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_network/test_transport.py +0 -0
  142. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_products/test_product_generator.py +0 -0
  143. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_products/test_registry.py +0 -0
  144. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_protocol/test_generated.py +0 -0
  145. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_protocol/test_header.py +0 -0
  146. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_protocol/test_protocol_generator.py +0 -0
  147. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_protocol/test_serializer.py +0 -0
  148. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_theme/__init__.py +0 -0
  149. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_theme/conftest.py +0 -0
  150. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_theme/test_apply_theme.py +0 -0
  151. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_theme/test_canvas.py +0 -0
  152. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_theme/test_generators.py +0 -0
  153. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_theme/test_library.py +0 -0
  154. {lifx_async-4.5.0 → lifx_async-4.5.1}/tests/test_theme/test_theme.py +0 -0
  155. {lifx_async-4.5.0 → lifx_async-4.5.1}/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.5.1
4
4
  Summary: A modern, type-safe, async Python library for controlling LIFX lights
5
5
  Author-email: Avi Miller <me@dje.li>
6
6
  Maintainer-email: Avi Miller <me@dje.li>
@@ -81,6 +81,19 @@ 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
+
84
97
  ## Device Properties
85
98
 
86
99
  ### MAC Address
@@ -268,3 +281,34 @@ async def main():
268
281
  # Stop the effect
269
282
  await light.set_effect(effect_type=FirmwareEffect.OFF)
270
283
  ```
284
+
285
+ ### Ceiling Light Control
286
+
287
+ ```python
288
+ from lifx import CeilingLight, HSBK
289
+
290
+
291
+ async def main():
292
+ async with await CeilingLight.from_ip("192.168.1.100") as ceiling:
293
+ # Set downlight to warm white
294
+ await ceiling.set_downlight_colors(
295
+ HSBK(hue=0, saturation=0, brightness=0.8, kelvin=3000)
296
+ )
297
+
298
+ # Set uplight to a dim ambient glow
299
+ await ceiling.set_uplight_color(
300
+ HSBK(hue=30, saturation=0.2, brightness=0.3, kelvin=2700)
301
+ )
302
+
303
+ # Turn uplight off (stores color for later restoration)
304
+ await ceiling.turn_uplight_off()
305
+
306
+ # Turn uplight back on (restores previous color)
307
+ await ceiling.turn_uplight_on()
308
+
309
+ # Check component state
310
+ if ceiling.downlight_is_on:
311
+ print("Downlight is currently on")
312
+ ```
313
+
314
+ For detailed CeilingLight usage, see the [Ceiling Lights User Guide](../user-guide/ceiling-lights.md).
@@ -2,6 +2,14 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v4.5.1 (2025-12-11)
6
+
7
+ ### Bug Fixes
8
+
9
+ - **devices**: Export CeilingLight add add user guide and API documentation
10
+ ([`10e0089`](https://github.com/Djelibeybi/lifx-async/commit/10e008983ffd8b233dd2427a4a4f64661c8f14bd))
11
+
12
+
5
13
  ## v4.5.0 (2025-12-08)
6
14
 
7
15
  ### 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,293 @@
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
+ ## Zone Layout
132
+
133
+ Access the component zone indices directly:
134
+
135
+ ```python
136
+ async with await CeilingLight.from_ip("192.168.1.100") as ceiling:
137
+ # Get uplight zone index (63 or 127 depending on model)
138
+ uplight_idx = ceiling.uplight_zone
139
+ print(f"Uplight zone: {uplight_idx}")
140
+
141
+ # Get downlight zones as a slice
142
+ downlight_slice = ceiling.downlight_zones
143
+ print(f"Downlight zones: {downlight_slice}") # slice(0, 63) or slice(0, 127)
144
+
145
+ # Calculate number of downlight zones
146
+ zone_count = len(range(*downlight_slice.indices(256)))
147
+ print(f"Number of downlight zones: {zone_count}")
148
+ ```
149
+
150
+ ## State Persistence
151
+
152
+ CeilingLight supports optional state persistence to preserve component colors across sessions:
153
+
154
+ ```python
155
+ async with await CeilingLight.from_ip(
156
+ "192.168.1.100",
157
+ state_file="~/.lifx/ceiling_state.json"
158
+ ) as ceiling:
159
+ # Colors are automatically loaded from file on connection
160
+ # and saved when using turn_*_off() methods
161
+
162
+ await ceiling.turn_uplight_off() # Saves current color to file
163
+ # ... later ...
164
+ await ceiling.turn_uplight_on() # Restores from file if available
165
+ ```
166
+
167
+ The state file stores colors per device serial number, supporting multiple devices:
168
+
169
+ ```json
170
+ {
171
+ "d073d5123456": {
172
+ "uplight": {
173
+ "hue": 30.0,
174
+ "saturation": 0.2,
175
+ "brightness": 0.4,
176
+ "kelvin": 2700
177
+ },
178
+ "downlight": [
179
+ {"hue": 0.0, "saturation": 0.0, "brightness": 0.8, "kelvin": 4000}
180
+ ]
181
+ }
182
+ }
183
+ ```
184
+
185
+ ## Brightness Determination
186
+
187
+ When calling `turn_uplight_on()` or `turn_downlight_on()` without a color parameter, CeilingLight uses the following priority to determine brightness:
188
+
189
+ 1. **Stored state**: If a color was previously saved (via `turn_*_off()` or `set_*_color()`)
190
+ 2. **Infer from other component**: Average brightness of the other component
191
+ 3. **Default**: 80% brightness
192
+
193
+ This ensures a reasonable brightness level even when no state is available.
194
+
195
+ ## Transition Duration
196
+
197
+ All color-setting methods support smooth transitions:
198
+
199
+ ```python
200
+ # 2-second transition to new color
201
+ await ceiling.set_uplight_color(
202
+ HSBK(hue=0, saturation=0, brightness=1.0, kelvin=3500),
203
+ duration=2.0 # seconds
204
+ )
205
+
206
+ # Instant change (default)
207
+ await ceiling.set_downlight_colors(
208
+ HSBK(hue=240, saturation=1.0, brightness=0.5, kelvin=3500),
209
+ duration=0.0
210
+ )
211
+ ```
212
+
213
+ ## MatrixLight Compatibility
214
+
215
+ CeilingLight extends `MatrixLight`, so all matrix operations are available:
216
+
217
+ ```python
218
+ async with await CeilingLight.from_ip("192.168.1.100") as ceiling:
219
+ # Use MatrixLight methods directly
220
+ all_colors = await ceiling.get_all_tile_colors()
221
+ tile_chain = await ceiling.get_tile_chain()
222
+
223
+ # Set raw matrix colors (bypasses component abstraction)
224
+ await ceiling.set_matrix_colors(0, colors)
225
+
226
+ # Apply effects
227
+ from lifx.protocol.protocol_types import TileEffectType
228
+ await ceiling.set_tile_effect(
229
+ effect_type=TileEffectType.MORPH,
230
+ speed=5000,
231
+ )
232
+ ```
233
+
234
+ ## Example: Night Mode
235
+
236
+ Create a subtle night light with dim uplight and downlight off:
237
+
238
+ ```python
239
+ from lifx import CeilingLight
240
+ from lifx.color import HSBK
241
+
242
+ async def night_mode(ip: str):
243
+ async with await CeilingLight.from_ip(ip) as ceiling:
244
+ # Store current colors before turning off
245
+ await ceiling.turn_downlight_off()
246
+
247
+ # Set uplight to very dim warm glow
248
+ await ceiling.set_uplight_color(
249
+ HSBK(hue=30, saturation=0.3, brightness=0.05, kelvin=2200),
250
+ duration=2.0
251
+ )
252
+ ```
253
+
254
+ ## Example: Daytime Productivity
255
+
256
+ Bright, cool white for focus:
257
+
258
+ ```python
259
+ async def daytime_mode(ip: str):
260
+ async with await CeilingLight.from_ip(ip) as ceiling:
261
+ # Bright cool downlight for task lighting
262
+ await ceiling.set_downlight_colors(
263
+ HSBK(hue=0, saturation=0, brightness=1.0, kelvin=5500),
264
+ duration=1.0
265
+ )
266
+
267
+ # Turn off uplight during the day
268
+ await ceiling.turn_uplight_off(duration=1.0)
269
+ ```
270
+
271
+ ## Example: Evening Ambiance
272
+
273
+ Warm tones with accent uplight:
274
+
275
+ ```python
276
+ async def evening_mode(ip: str):
277
+ async with await CeilingLight.from_ip(ip) as ceiling:
278
+ # Dimmed warm downlight
279
+ await ceiling.set_downlight_colors(
280
+ HSBK(hue=30, saturation=0.1, brightness=0.4, kelvin=2700),
281
+ duration=2.0
282
+ )
283
+
284
+ # Colorful uplight accent
285
+ await ceiling.set_uplight_color(
286
+ HSBK(hue=280, saturation=0.6, brightness=0.3, kelvin=3500),
287
+ duration=2.0
288
+ )
289
+ ```
290
+
291
+ ## API Reference
292
+
293
+ 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.5.1"
4
4
  description = "A modern, type-safe, async Python library for controlling LIFX lights"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -16,6 +16,7 @@ from lifx.api import (
16
16
  )
17
17
  from lifx.color import HSBK, Colors
18
18
  from lifx.devices import (
19
+ CeilingLight,
19
20
  Device,
20
21
  DeviceInfo,
21
22
  DeviceVersion,
@@ -71,6 +72,7 @@ __all__ = [
71
72
  "MultiZoneLightState",
72
73
  "MatrixLight",
73
74
  "MatrixLightState",
75
+ "CeilingLight",
74
76
  # Color
75
77
  "HSBK",
76
78
  "Colors",
@@ -354,10 +354,7 @@ class MatrixLight(Light):
354
354
  def __init__(self, *args, **kwargs) -> None:
355
355
  """Initialize MatrixLight device.
356
356
 
357
- Args:
358
- serial: Device serial number
359
- ip: Device IP address
360
- port: Device port (default: 56700)
357
+ See :class:`Light` for parameter documentation.
361
358
  """
362
359
  super().__init__(*args, **kwargs)
363
360
  # Matrix specific properties