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.
- {lifx_async-4.0.2 → lifx_async-4.2.0}/CLAUDE.md +31 -2
- {lifx_async-4.0.2 → lifx_async-4.2.0}/PKG-INFO +1 -1
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/devices.md +29 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/network.md +0 -11
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/changelog.md +21 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/pyproject.toml +2 -2
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/color.py +0 -24
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/base.py +17 -31
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/light.py +39 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/matrix.py +3 -7
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/network/__init__.py +1 -2
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/network/connection.py +354 -162
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/network/discovery.py +18 -8
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/network/message.py +3 -74
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/generator.py +38 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/packets.py +35 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/theme/theme.py +3 -1
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_light.py +34 -0
- lifx_async-4.2.0/tests/test_network/test_connection.py +548 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/test_message.py +1 -84
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/test_message_advanced.py +2 -27
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_protocol/test_generated.py +46 -1
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_protocol/test_protocol_generator.py +63 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/uv.lock +5 -5
- lifx_async-4.0.2/tests/test_network/test_connection.py +0 -1109
- {lifx_async-4.0.2 → lifx_async-4.2.0}/.claude/settings.json +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/.github/dependabot.yml +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/.github/labeler.yml +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/.github/workflows/ci.yml +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/.github/workflows/docs.yml +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/.gitignore +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/.pre-commit-config.yaml +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/LICENSE +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/README.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/colors.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/effects.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/exceptions.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/high-level.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/index.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/protocol.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/api/themes.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/architecture/overview.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/faq.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/getting-started/effects.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/getting-started/installation.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/getting-started/themes.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/index.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/stylesheets/extra.css +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/user-guide/advanced-usage.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/user-guide/themes.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/01_simple_discovery.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/02_simple_control.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/03_waveforms.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/04_logging.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/06_pulse_effect.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/08_custom_effect.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/09_background_effect.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/11_matrix_basic.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/12_matrix_effects.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/examples/13_matrix_large.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/mkdocs.yml +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/renovate.json +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/__init__.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/api.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/const.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/__init__.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/hev.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/infrared.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/devices/multizone.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/base.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/colorloop.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/const.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/models.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/exceptions.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/network/transport.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/products/__init__.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/products/generator.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/products/registry.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/base.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/header.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/models.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/protocol_types.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/py.typed +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/theme/generators.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/src/lifx/theme/library.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/__init__.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/conftest.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_api/__init__.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_api/test_api_apply_theme.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_api/test_api_batch_operations.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_api/test_api_discovery.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_color.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/__init__.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/conftest.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_base.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_matrix.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_devices/test_multizone.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/__init__.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_base.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_models.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/__init__.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/test_concurrent_requests.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_network/test_transport.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_products/test_registry.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_protocol/test_serializer.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/__init__.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/conftest.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/test_canvas.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/test_library.py +0 -0
- {lifx_async-4.0.2 → lifx_async-4.2.0}/tests/test_theme/test_theme.py +0 -0
- {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
|
|
|
@@ -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
|
|
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.
|
|
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
|
|
359
|
-
"""Calculate
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
376
|
-
|
|
370
|
+
if firmware.version_major == 3:
|
|
371
|
+
octets[5] = (octets[5] + 1) % 256
|
|
377
372
|
|
|
378
|
-
|
|
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
|
-
|
|
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.
|
|
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__(
|
|
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
|
|
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
|