lifx-async 4.0.2__tar.gz → 4.2.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 (144) hide show
  1. {lifx_async-4.0.2 → lifx_async-4.2.0}/CLAUDE.md +31 -2
  2. {lifx_async-4.0.2 → lifx_async-4.2.0}/PKG-INFO +1 -1
  3. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/devices.md +29 -0
  4. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/network.md +0 -11
  5. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/changelog.md +21 -0
  6. {lifx_async-4.0.2 → lifx_async-4.2.0}/pyproject.toml +2 -2
  7. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/color.py +0 -24
  8. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/base.py +17 -31
  9. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/light.py +39 -0
  10. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/matrix.py +3 -7
  11. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/network/__init__.py +1 -2
  12. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/network/connection.py +354 -162
  13. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/network/discovery.py +18 -8
  14. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/network/message.py +3 -74
  15. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/generator.py +38 -0
  16. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/packets.py +35 -0
  17. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/theme/theme.py +3 -1
  18. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_light.py +34 -0
  19. lifx_async-4.2.0/tests/test_network/test_connection.py +548 -0
  20. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/test_message.py +1 -84
  21. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/test_message_advanced.py +2 -27
  22. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_protocol/test_generated.py +46 -1
  23. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_protocol/test_protocol_generator.py +63 -0
  24. {lifx_async-4.0.2 → lifx_async-4.2.0}/uv.lock +5 -5
  25. lifx_async-4.0.2/tests/test_network/test_connection.py +0 -1109
  26. {lifx_async-4.0.2 → lifx_async-4.2.0}/.claude/settings.json +0 -0
  27. {lifx_async-4.0.2 → lifx_async-4.2.0}/.github/dependabot.yml +0 -0
  28. {lifx_async-4.0.2 → lifx_async-4.2.0}/.github/labeler.yml +0 -0
  29. {lifx_async-4.0.2 → lifx_async-4.2.0}/.github/workflows/ci.yml +0 -0
  30. {lifx_async-4.0.2 → lifx_async-4.2.0}/.github/workflows/docs.yml +0 -0
  31. {lifx_async-4.0.2 → lifx_async-4.2.0}/.github/workflows/pr-automation.yml +0 -0
  32. {lifx_async-4.0.2 → lifx_async-4.2.0}/.gitignore +0 -0
  33. {lifx_async-4.0.2 → lifx_async-4.2.0}/.pre-commit-config.yaml +0 -0
  34. {lifx_async-4.0.2 → lifx_async-4.2.0}/LICENSE +0 -0
  35. {lifx_async-4.0.2 → lifx_async-4.2.0}/README.md +0 -0
  36. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/colors.md +0 -0
  37. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/effects.md +0 -0
  38. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/exceptions.md +0 -0
  39. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/high-level.md +0 -0
  40. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/index.md +0 -0
  41. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/protocol.md +0 -0
  42. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/themes.md +0 -0
  43. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/architecture/effects-architecture.md +0 -0
  44. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/architecture/overview.md +0 -0
  45. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/faq.md +0 -0
  46. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/getting-started/effects.md +0 -0
  47. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/getting-started/installation.md +0 -0
  48. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/getting-started/quickstart.md +0 -0
  49. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/getting-started/themes.md +0 -0
  50. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/index.md +0 -0
  51. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/stylesheets/extra.css +0 -0
  52. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/user-guide/advanced-usage.md +0 -0
  53. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/user-guide/effects-custom.md +0 -0
  54. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/user-guide/effects-troubleshooting.md +0 -0
  55. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/user-guide/protocol-deep-dive.md +0 -0
  56. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/user-guide/themes.md +0 -0
  57. {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/user-guide/troubleshooting.md +0 -0
  58. {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/01_simple_discovery.py +0 -0
  59. {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/02_simple_control.py +0 -0
  60. {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/03_waveforms.py +0 -0
  61. {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/04_logging.py +0 -0
  62. {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/06_pulse_effect.py +0 -0
  63. {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/07_colorloop_effect.py +0 -0
  64. {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/08_custom_effect.py +0 -0
  65. {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/09_background_effect.py +0 -0
  66. {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/10_find_specific_devices.py +0 -0
  67. {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/11_matrix_basic.py +0 -0
  68. {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/12_matrix_effects.py +0 -0
  69. {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/13_matrix_large.py +0 -0
  70. {lifx_async-4.0.2 → lifx_async-4.2.0}/mkdocs.yml +0 -0
  71. {lifx_async-4.0.2 → lifx_async-4.2.0}/renovate.json +0 -0
  72. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/__init__.py +0 -0
  73. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/api.py +0 -0
  74. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/const.py +0 -0
  75. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/__init__.py +0 -0
  76. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/hev.py +0 -0
  77. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/infrared.py +0 -0
  78. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/multizone.py +0 -0
  79. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/__init__.py +0 -0
  80. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/base.py +0 -0
  81. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/colorloop.py +0 -0
  82. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/conductor.py +0 -0
  83. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/const.py +0 -0
  84. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/models.py +0 -0
  85. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/pulse.py +0 -0
  86. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/state_manager.py +0 -0
  87. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/exceptions.py +0 -0
  88. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/network/transport.py +0 -0
  89. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/products/__init__.py +0 -0
  90. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/products/generator.py +0 -0
  91. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/products/registry.py +0 -0
  92. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/__init__.py +0 -0
  93. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/base.py +0 -0
  94. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/header.py +0 -0
  95. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/models.py +0 -0
  96. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/protocol_types.py +0 -0
  97. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/serializer.py +0 -0
  98. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/py.typed +0 -0
  99. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/theme/__init__.py +0 -0
  100. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/theme/canvas.py +0 -0
  101. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/theme/generators.py +0 -0
  102. {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/theme/library.py +0 -0
  103. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/__init__.py +0 -0
  104. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/conftest.py +0 -0
  105. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_api/__init__.py +0 -0
  106. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_api/test_api_apply_theme.py +0 -0
  107. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_api/test_api_batch_errors.py +0 -0
  108. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_api/test_api_batch_operations.py +0 -0
  109. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_api/test_api_discovery.py +0 -0
  110. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_api/test_api_organization.py +0 -0
  111. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_color.py +0 -0
  112. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/__init__.py +0 -0
  113. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/conftest.py +0 -0
  114. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_base.py +0 -0
  115. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_hev.py +0 -0
  116. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_infrared.py +0 -0
  117. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_mac_address.py +0 -0
  118. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_matrix.py +0 -0
  119. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_multizone.py +0 -0
  120. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/__init__.py +0 -0
  121. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_base.py +0 -0
  122. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_capability_filtering.py +0 -0
  123. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_colorloop.py +0 -0
  124. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_integration.py +0 -0
  125. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_models.py +0 -0
  126. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_pulse.py +0 -0
  127. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_state_manager.py +0 -0
  128. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/__init__.py +0 -0
  129. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/test_concurrent_requests.py +0 -0
  130. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/test_discovery_devices.py +0 -0
  131. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/test_discovery_errors.py +0 -0
  132. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/test_transport.py +0 -0
  133. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_products/test_product_generator.py +0 -0
  134. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_products/test_registry.py +0 -0
  135. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_protocol/test_header.py +0 -0
  136. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_protocol/test_serializer.py +0 -0
  137. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/__init__.py +0 -0
  138. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/conftest.py +0 -0
  139. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/test_apply_theme.py +0 -0
  140. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/test_canvas.py +0 -0
  141. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/test_generators.py +0 -0
  142. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/test_library.py +0 -0
  143. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/test_theme.py +0 -0
  144. {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_utils.py +0 -0
@@ -209,7 +209,7 @@ except LifxDeviceNotFoundError:
209
209
  **Current Behavior**:
210
210
  - Selected properties cache static/semi-static values to reduce network requests
211
211
  - Cached properties: `label`, `version`, `host_firmware`, `wifi_firmware`, `location`, `group`, `hev_config`, `hev_result`, `zone_count`, `multizone_effect`, `tile_chain`, `tile_count`, `tile_effect`
212
- - Volatile state (power, color, hev_cycle, zones, tile_colors) is **not** cached - always use `get_*()` methods to fetch fresh data
212
+ - Volatile state (power, color, hev_cycle, zones, tile_colors, ambient_light_level) is **not** cached - always use `get_*()` methods to fetch fresh data
213
213
  - Use `get_*()` methods to fetch fresh data from devices for any property
214
214
  - No automatic expiration - application controls when to refresh
215
215
  - Use `get_color()` to retrieve color, power, and label values as two of the three are volatile and it returns all three in a single request/response pair.
@@ -228,7 +228,7 @@ async with device:
228
228
  is_on = power_level > 0
229
229
  ```
230
230
 
231
- **Note**: Volatile state properties (`power`, `color`, `hev_cycle`, `zones`, `tile_colors`) were removed as they change too frequently to benefit from caching. Always fetch these values using `get_*()` methods.
231
+ **Note**: Volatile state properties (`power`, `color`, `hev_cycle`, `zones`, `tile_colors`, `ambient_light_level`) were removed as they change too frequently to benefit from caching. Always fetch these values using `get_*()` methods.
232
232
 
233
233
  ## Common Patterns
234
234
 
@@ -404,6 +404,31 @@ async with await InfraredLight.from_ip("192.168.1.100") as light:
404
404
  print(f"IR brightness: {brightness * 100}%")
405
405
  ```
406
406
 
407
+ ### Ambient Light Sensor (Light Level Detection)
408
+
409
+ Light devices with ambient light sensors can measure the current ambient light level in lux:
410
+
411
+ ```python
412
+ from lifx.devices import Light
413
+
414
+ async with await Light.from_ip("192.168.1.100") as light:
415
+ # Turn light off for accurate reading
416
+ await light.set_power(False)
417
+
418
+ # Get ambient light level in lux
419
+ lux = await light.get_ambient_light_level()
420
+ if lux > 0:
421
+ print(f"Ambient light: {lux} lux")
422
+ else:
423
+ print("No ambient light sensor or completely dark")
424
+ ```
425
+
426
+ **Notes:**
427
+ - This is a volatile property and is never cached - always fetched fresh from the device
428
+ - Devices without ambient light sensors return 0.0 (not an error)
429
+ - For accurate readings, the light should be off - otherwise the light's own illumination interferes with the sensor
430
+ - A reading of 0.0 could mean either no sensor or complete darkness
431
+
407
432
  ### MultiZone Light Control (Strips and Beams)
408
433
 
409
434
  MultiZoneLight devices support zone-based color control:
@@ -695,6 +720,10 @@ Local generator quirks:
695
720
  - Unions starting with "Button" or "Relay" are excluded
696
721
  - All packets in "button" and "relay" categories are excluded
697
722
  - This keeps the library focused on LIFX lighting devices
723
+ - **sensor packets**: Adds undocumented ambient light sensor packets:
724
+ - `SensorGetAmbientLight` (401): Request packet with no parameters
725
+ - `SensorStateAmbientLight` (402): Response packet with lux field (float32)
726
+ - These packets are not in the official protocol.yml but are supported by LIFX devices with ambient light sensors
698
727
 
699
728
  Run `uv run python -m lifx.protocol.generator` to regenerate Python code.
700
729
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-async
3
- Version: 4.0.2
3
+ Version: 4.2.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>
@@ -182,6 +182,35 @@ async def main():
182
182
  print(f"IR brightness: {brightness * 100}%")
183
183
  ```
184
184
 
185
+ ### Ambient Light Sensor
186
+
187
+ Light devices with ambient light sensors can measure the current ambient light level in lux:
188
+
189
+ ```python
190
+ from lifx import Light
191
+
192
+
193
+ async def main():
194
+ async with await Light.from_ip("192.168.1.100") as light:
195
+ # Ensure light is off for accurate reading
196
+ await light.set_power(False)
197
+
198
+ # Get ambient light level in lux
199
+ lux = await light.get_ambient_light_level()
200
+ if lux > 0:
201
+ print(f"Ambient light: {lux} lux")
202
+ else:
203
+ print("No ambient light sensor or completely dark")
204
+ ```
205
+
206
+ **Notes:**
207
+
208
+ - Devices without ambient light sensors return 0.0 (not an error)
209
+ - For accurate readings, the light should be turned off (otherwise the light's own illumination interferes with the sensor)
210
+ - This is a volatile property - always fetched fresh from the device
211
+ - A reading of 0.0 could mean either no sensor or complete darkness
212
+ - Returns ambient light level in lux (higher values indicate brighter ambient light)
213
+
185
214
  ### MultiZone Control
186
215
 
187
216
  ```python
@@ -31,17 +31,6 @@ Low-level UDP transport for sending and receiving LIFX protocol messages.
31
31
  filters:
32
32
  - "!^_"
33
33
 
34
- ## Message Building
35
-
36
- Utilities for building and parsing LIFX protocol messages.
37
-
38
- ::: lifx.network.message.MessageBuilder
39
- options:
40
- show_root_heading: true
41
- heading_level: 3
42
- members_order: source
43
- show_if_no_docstring: false
44
-
45
34
  ## Examples
46
35
 
47
36
  ### Device Discovery
@@ -2,6 +2,27 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v4.2.0 (2025-11-21)
6
+
7
+ ### Documentation
8
+
9
+ - **api**: Remove obsolete reference to MessageBuilder
10
+ ([`9847948`](https://github.com/Djelibeybi/lifx-async/commit/98479483d00c875e324d5a7dcd88bf08f11f73cb))
11
+
12
+ ### Features
13
+
14
+ - **devices**: Add ambient light sensor support
15
+ ([`75f0673`](https://github.com/Djelibeybi/lifx-async/commit/75f0673dc9b6e8bce30a5b5958215a600925357e))
16
+
17
+
18
+ ## v4.1.0 (2025-11-20)
19
+
20
+ ### Features
21
+
22
+ - **network**: Replace polling architecture with event-driven background receiver
23
+ ([`9862eac`](https://github.com/Djelibeybi/lifx-async/commit/9862eac1eea162fa66bf19d277a3772de7c70db1))
24
+
25
+
5
26
  ## v4.0.2 (2025-11-19)
6
27
 
7
28
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lifx-async"
3
- version = "4.0.2"
3
+ version = "4.2.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"
@@ -31,7 +31,7 @@ classifiers = [
31
31
  [dependency-groups]
32
32
  dev = [
33
33
  "hatchling>=1.27.0",
34
- "lifx-emulator>=2.3.1",
34
+ "lifx-emulator>=2.4.0",
35
35
  "mkdocs-git-revision-date-localized-plugin>=1.4.7",
36
36
  "mkdocs-llmstxt>=0.4.0",
37
37
  "mkdocs-material>=9.6.22",
@@ -119,30 +119,6 @@ class HSBK:
119
119
  self._brightness = brightness
120
120
  self._kelvin = kelvin
121
121
 
122
- def __lt__(self, other: object) -> bool:
123
- """A color is less than another color if it has lower HSBK values."""
124
- if not isinstance(other, HSBK): # pragma: no cover
125
- return NotImplemented
126
-
127
- return (self.hue, self.saturation, self.brightness, self.kelvin) < (
128
- other.hue,
129
- other.saturation,
130
- other.brightness,
131
- other.kelvin,
132
- )
133
-
134
- def __gt__(self, other: object) -> bool:
135
- """A color is more than another color if it has higher HSBK values."""
136
- if not isinstance(other, HSBK): # pragma: no cover
137
- return NotImplemented
138
-
139
- return (self.hue, self.saturation, self.brightness, self.kelvin) > (
140
- other.hue,
141
- other.saturation,
142
- other.brightness,
143
- other.kelvin,
144
- )
145
-
146
122
  def __eq__(self, other: object) -> bool:
147
123
  """Two colors are equal if they have the same HSBK values."""
148
124
  if not isinstance(other, HSBK): # pragma: no cover
@@ -355,39 +355,24 @@ class Device:
355
355
  tg.create_task(self.get_location())
356
356
  tg.create_task(self.get_group())
357
357
 
358
- def _calculate_mac_address(self) -> None:
359
- """Calculate MAC address from serial and host firmware version.
360
-
361
- The MAC address calculation depends on the major version of the host firmware:
362
- - Version 2 or 4: MAC address matches the serial
363
- - Version 3: MAC address is serial with LSB + 1 (with wraparound from FF to 00)
364
- - Unknown versions: Default to serial
365
-
366
- This method is called automatically when host firmware is fetched.
367
- """
368
- if self._host_firmware is None: # pragma: no cover
369
- return
370
-
371
- # Get serial bytes
372
- serial_obj = Serial.from_string(self.serial)
373
- serial_bytes = bytearray(serial_obj.value)
358
+ async def get_mac_address(self) -> str:
359
+ """Calculate and return the MAC address for this device."""
360
+ if self._mac_address is None:
361
+ firmware = (
362
+ self._host_firmware
363
+ if self._host_firmware is not None
364
+ else await self.get_host_firmware()
365
+ )
366
+ octets = [
367
+ int(self.serial[i : i + 2], 16) for i in range(0, len(self.serial), 2)
368
+ ]
374
369
 
375
- # Check firmware major version
376
- major_version = self._host_firmware.version_major
370
+ if firmware.version_major == 3:
371
+ octets[5] = (octets[5] + 1) % 256
377
372
 
378
- if major_version in (2, 4):
379
- # MAC address matches serial
380
- mac_bytes = bytes(serial_bytes)
381
- elif major_version == 3:
382
- # Add 1 to least significant byte (with wraparound)
383
- serial_bytes[5] = (serial_bytes[5] + 1) % 256
384
- mac_bytes = bytes(serial_bytes)
385
- else:
386
- # For unknown versions, default to serial
387
- mac_bytes = bytes(serial_bytes)
373
+ self._mac_address = ":".join(f"{octet:02x}" for octet in octets)
388
374
 
389
- # Convert to colon-separated hex string format (e.g., "d0:73:d5:01:02:03")
390
- self._mac_address = ":".join(f"{b:02x}" for b in mac_bytes)
375
+ return self._mac_address
391
376
 
392
377
  async def _ensure_capabilities(self) -> None:
393
378
  """Ensure device capabilities are populated.
@@ -753,7 +738,8 @@ class Device:
753
738
  self._host_firmware = firmware
754
739
 
755
740
  # Calculate MAC address now that we have firmware info
756
- self._calculate_mac_address()
741
+ if self.mac_address is None:
742
+ await self.get_mac_address()
757
743
 
758
744
  _LOGGER.debug(
759
745
  {
@@ -379,6 +379,45 @@ class Light(Device):
379
379
 
380
380
  return state.level
381
381
 
382
+ async def get_ambient_light_level(self) -> float:
383
+ """Get ambient light level from device sensor.
384
+
385
+ Always fetches from device (volatile property, not cached).
386
+
387
+ This method queries the device's ambient light sensor to get the current
388
+ lux reading. Devices without ambient light sensors will return 0.0.
389
+
390
+ Returns:
391
+ Ambient light level in lux (0.0 if device has no sensor)
392
+
393
+ Raises:
394
+ LifxDeviceNotFoundError: If device is not connected
395
+ LifxTimeoutError: If device does not respond
396
+ LifxProtocolError: If response is invalid
397
+
398
+ Example:
399
+ ```python
400
+ lux = await light.get_ambient_light_level()
401
+ if lux > 0:
402
+ print(f"Ambient light: {lux} lux")
403
+ else:
404
+ print("No ambient light sensor or completely dark")
405
+ ```
406
+ """
407
+ # Request automatically unpacks response
408
+ state = await self.connection.request(packets.Sensor.GetAmbientLight())
409
+
410
+ _LOGGER.debug(
411
+ {
412
+ "class": "Light",
413
+ "method": "get_ambient_light_level",
414
+ "action": "query",
415
+ "reply": {"lux": state.lux},
416
+ }
417
+ )
418
+
419
+ return state.lux
420
+
382
421
  async def set_power(self, level: bool | int, duration: float = 0.0) -> None:
383
422
  """Set light power state (specific to light, not device).
384
423
 
@@ -285,12 +285,7 @@ class MatrixLight(Light):
285
285
  ... await matrix.set64(tile_index=0, colors=colors, width=8)
286
286
  """
287
287
 
288
- def __init__(
289
- self,
290
- serial: str,
291
- ip: str,
292
- port: int = 56700,
293
- ) -> None:
288
+ def __init__(self, *args, **kwargs) -> None:
294
289
  """Initialize MatrixLight device.
295
290
 
296
291
  Args:
@@ -298,7 +293,8 @@ class MatrixLight(Light):
298
293
  ip: Device IP address
299
294
  port: Device port (default: 56700)
300
295
  """
301
- super().__init__(serial, ip, port)
296
+ super().__init__(*args, **kwargs)
297
+ # Matrix specific properties
302
298
  self._device_chain: list[TileInfo] | None = None
303
299
  self._tile_effect: MatrixEffect | None = None
304
300
 
@@ -2,14 +2,13 @@
2
2
 
3
3
  from lifx.network.connection import DeviceConnection
4
4
  from lifx.network.discovery import DiscoveredDevice, discover_devices
5
- from lifx.network.message import MessageBuilder, create_message, parse_message
5
+ from lifx.network.message import create_message, parse_message
6
6
  from lifx.network.transport import UdpTransport
7
7
 
8
8
  __all__ = [
9
9
  # Transport
10
10
  "UdpTransport",
11
11
  # Message
12
- "MessageBuilder",
13
12
  "create_message",
14
13
  "parse_message",
15
14
  # Discovery