lifx-async 4.7.4__py3-none-any.whl → 4.8.0__py3-none-any.whl
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/__init__.py +6 -0
- lifx/api.py +54 -0
- lifx/const.py +13 -0
- lifx/devices/ceiling.py +75 -17
- lifx/network/mdns/__init__.py +58 -0
- lifx/network/mdns/discovery.py +403 -0
- lifx/network/mdns/dns.py +356 -0
- lifx/network/mdns/transport.py +313 -0
- lifx/network/mdns/types.py +35 -0
- {lifx_async-4.7.4.dist-info → lifx_async-4.8.0.dist-info}/METADATA +1 -1
- {lifx_async-4.7.4.dist-info → lifx_async-4.8.0.dist-info}/RECORD +13 -8
- {lifx_async-4.7.4.dist-info → lifx_async-4.8.0.dist-info}/WHEEL +0 -0
- {lifx_async-4.7.4.dist-info → lifx_async-4.8.0.dist-info}/licenses/LICENSE +0 -0
lifx/__init__.py
CHANGED
|
@@ -10,6 +10,7 @@ from importlib.metadata import version as get_version
|
|
|
10
10
|
from lifx.api import (
|
|
11
11
|
DeviceGroup,
|
|
12
12
|
discover,
|
|
13
|
+
discover_mdns,
|
|
13
14
|
find_by_ip,
|
|
14
15
|
find_by_label,
|
|
15
16
|
find_by_serial,
|
|
@@ -48,6 +49,7 @@ from lifx.exceptions import (
|
|
|
48
49
|
LifxUnsupportedCommandError,
|
|
49
50
|
)
|
|
50
51
|
from lifx.network.discovery import DiscoveredDevice, discover_devices
|
|
52
|
+
from lifx.network.mdns import LifxServiceRecord, discover_lifx_services
|
|
51
53
|
from lifx.products import ProductCapability, ProductInfo, ProductRegistry
|
|
52
54
|
from lifx.protocol.protocol_types import (
|
|
53
55
|
Direction,
|
|
@@ -100,12 +102,16 @@ __all__ = [
|
|
|
100
102
|
# High-level API
|
|
101
103
|
"DeviceGroup",
|
|
102
104
|
"discover",
|
|
105
|
+
"discover_mdns",
|
|
103
106
|
"find_by_serial",
|
|
104
107
|
"find_by_label",
|
|
105
108
|
"find_by_ip",
|
|
106
109
|
# Discovery (low-level)
|
|
107
110
|
"discover_devices",
|
|
108
111
|
"DiscoveredDevice",
|
|
112
|
+
# mDNS Discovery (low-level)
|
|
113
|
+
"discover_lifx_services",
|
|
114
|
+
"LifxServiceRecord",
|
|
109
115
|
# Products
|
|
110
116
|
"ProductInfo",
|
|
111
117
|
"ProductRegistry",
|
lifx/api.py
CHANGED
|
@@ -803,6 +803,59 @@ async def discover(
|
|
|
803
803
|
yield device
|
|
804
804
|
|
|
805
805
|
|
|
806
|
+
async def discover_mdns(
|
|
807
|
+
timeout: float = DISCOVERY_TIMEOUT,
|
|
808
|
+
max_response_time: float = MAX_RESPONSE_TIME,
|
|
809
|
+
idle_timeout_multiplier: float = IDLE_TIMEOUT_MULTIPLIER,
|
|
810
|
+
device_timeout: float = DEFAULT_REQUEST_TIMEOUT,
|
|
811
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
812
|
+
) -> AsyncGenerator[Light, None]:
|
|
813
|
+
"""Discover LIFX devices via mDNS and yield them as they are found.
|
|
814
|
+
|
|
815
|
+
Uses mDNS/DNS-SD discovery with the _lifx._udp.local service type.
|
|
816
|
+
This method is faster than broadcast discovery as device type information
|
|
817
|
+
is included in the mDNS TXT records, eliminating the need for additional
|
|
818
|
+
device queries.
|
|
819
|
+
|
|
820
|
+
Note: mDNS discovery requires the mDNS multicast group (224.0.0.251:5353)
|
|
821
|
+
to be accessible. Some network configurations may block multicast traffic.
|
|
822
|
+
|
|
823
|
+
Args:
|
|
824
|
+
timeout: Discovery timeout in seconds (default 15.0)
|
|
825
|
+
max_response_time: Max time to wait for responses
|
|
826
|
+
idle_timeout_multiplier: Idle timeout multiplier
|
|
827
|
+
device_timeout: request timeout set on discovered devices
|
|
828
|
+
max_retries: max retries per request set on discovered devices
|
|
829
|
+
|
|
830
|
+
Yields:
|
|
831
|
+
Device instances as they are discovered
|
|
832
|
+
|
|
833
|
+
Example:
|
|
834
|
+
```python
|
|
835
|
+
# Process devices as they're discovered
|
|
836
|
+
async for device in discover_mdns():
|
|
837
|
+
print(f"Found: {device.serial}")
|
|
838
|
+
async with device:
|
|
839
|
+
await device.set_power(True)
|
|
840
|
+
|
|
841
|
+
# Or collect all devices first
|
|
842
|
+
devices = []
|
|
843
|
+
async for device in discover_mdns():
|
|
844
|
+
devices.append(device)
|
|
845
|
+
```
|
|
846
|
+
"""
|
|
847
|
+
from lifx.network.mdns.discovery import discover_devices_mdns
|
|
848
|
+
|
|
849
|
+
async for device in discover_devices_mdns(
|
|
850
|
+
timeout=timeout,
|
|
851
|
+
max_response_time=max_response_time,
|
|
852
|
+
idle_timeout_multiplier=idle_timeout_multiplier,
|
|
853
|
+
device_timeout=device_timeout,
|
|
854
|
+
max_retries=max_retries,
|
|
855
|
+
):
|
|
856
|
+
yield device
|
|
857
|
+
|
|
858
|
+
|
|
806
859
|
async def find_by_serial(
|
|
807
860
|
serial: str,
|
|
808
861
|
timeout: float = DISCOVERY_TIMEOUT,
|
|
@@ -996,6 +1049,7 @@ __all__ = [
|
|
|
996
1049
|
"LocationGrouping",
|
|
997
1050
|
"GroupGrouping",
|
|
998
1051
|
"discover",
|
|
1052
|
+
"discover_mdns",
|
|
999
1053
|
"find_by_serial",
|
|
1000
1054
|
"find_by_ip",
|
|
1001
1055
|
"find_by_label",
|
lifx/const.py
CHANGED
|
@@ -38,6 +38,19 @@ STATE_REFRESH_DEBOUNCE_MS: Final[int] = 300
|
|
|
38
38
|
# Default maximum number of retry attempts for failed requests
|
|
39
39
|
DEFAULT_MAX_RETRIES: Final[int] = 8
|
|
40
40
|
|
|
41
|
+
# ============================================================================
|
|
42
|
+
# mDNS Constants
|
|
43
|
+
# ============================================================================
|
|
44
|
+
|
|
45
|
+
# mDNS multicast address (IPv4)
|
|
46
|
+
MDNS_ADDRESS: Final[str] = "224.0.0.251"
|
|
47
|
+
|
|
48
|
+
# mDNS port
|
|
49
|
+
MDNS_PORT: Final[int] = 5353
|
|
50
|
+
|
|
51
|
+
# LIFX mDNS service type
|
|
52
|
+
LIFX_MDNS_SERVICE: Final[str] = "_lifx._udp.local"
|
|
53
|
+
|
|
41
54
|
# ============================================================================
|
|
42
55
|
# HSBK Min/Max Values
|
|
43
56
|
# ============================================================================
|
lifx/devices/ceiling.py
CHANGED
|
@@ -347,6 +347,22 @@ class CeilingLight(MatrixLight):
|
|
|
347
347
|
|
|
348
348
|
return layout.downlight_zones
|
|
349
349
|
|
|
350
|
+
@property
|
|
351
|
+
def downlight_zone_count(self) -> int:
|
|
352
|
+
"""Number of downlight zones.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Zone count (63 for standard 8x8, 127 for Capsule 16x8)
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
LifxError: If device version is not available or not a Ceiling product
|
|
359
|
+
"""
|
|
360
|
+
# downlight_zones is slice(0, N), so stop equals the count
|
|
361
|
+
stop = self.downlight_zones.stop
|
|
362
|
+
if stop is None:
|
|
363
|
+
raise LifxError("Invalid downlight zones configuration")
|
|
364
|
+
return stop
|
|
365
|
+
|
|
350
366
|
@property
|
|
351
367
|
def uplight_is_on(self) -> bool:
|
|
352
368
|
"""True if uplight component is currently on.
|
|
@@ -496,7 +512,7 @@ class CeilingLight(MatrixLight):
|
|
|
496
512
|
"Cannot set downlight color with brightness=0. "
|
|
497
513
|
"Use turn_downlight_off() instead."
|
|
498
514
|
)
|
|
499
|
-
downlight_colors = [colors] *
|
|
515
|
+
downlight_colors = [colors] * self.downlight_zone_count
|
|
500
516
|
else:
|
|
501
517
|
if all(c.brightness == 0 for c in colors):
|
|
502
518
|
raise ValueError(
|
|
@@ -504,10 +520,10 @@ class CeilingLight(MatrixLight):
|
|
|
504
520
|
"Use turn_downlight_off() instead."
|
|
505
521
|
)
|
|
506
522
|
|
|
507
|
-
|
|
508
|
-
if len(colors) != expected_count:
|
|
523
|
+
if len(colors) != self.downlight_zone_count:
|
|
509
524
|
raise ValueError(
|
|
510
|
-
f"Expected {
|
|
525
|
+
f"Expected {self.downlight_zone_count} colors for downlight, "
|
|
526
|
+
f"got {len(colors)}"
|
|
511
527
|
)
|
|
512
528
|
downlight_colors = colors
|
|
513
529
|
|
|
@@ -684,10 +700,6 @@ class CeilingLight(MatrixLight):
|
|
|
684
700
|
ValueError: If list length doesn't match downlight zone count
|
|
685
701
|
LifxTimeoutError: Device did not respond
|
|
686
702
|
"""
|
|
687
|
-
# Number of downlight zones equals the uplight zone index
|
|
688
|
-
# (downlight is zones 0 to uplight_zone-1)
|
|
689
|
-
downlight_zone_count = self.uplight_zone
|
|
690
|
-
|
|
691
703
|
# Validate provided colors early
|
|
692
704
|
if colors is not None:
|
|
693
705
|
if isinstance(colors, HSBK):
|
|
@@ -696,9 +708,9 @@ class CeilingLight(MatrixLight):
|
|
|
696
708
|
else:
|
|
697
709
|
if all(c.brightness == 0 for c in colors):
|
|
698
710
|
raise ValueError("Cannot turn on downlight with brightness=0")
|
|
699
|
-
if len(colors) != downlight_zone_count:
|
|
711
|
+
if len(colors) != self.downlight_zone_count:
|
|
700
712
|
raise ValueError(
|
|
701
|
-
f"Expected {downlight_zone_count} colors for downlight, "
|
|
713
|
+
f"Expected {self.downlight_zone_count} colors for downlight, "
|
|
702
714
|
f"got {len(colors)}"
|
|
703
715
|
)
|
|
704
716
|
|
|
@@ -711,7 +723,7 @@ class CeilingLight(MatrixLight):
|
|
|
711
723
|
# Determine target colors (pass pre-fetched colors to avoid extra fetch)
|
|
712
724
|
if colors is not None:
|
|
713
725
|
if isinstance(colors, HSBK):
|
|
714
|
-
target_colors = [colors] * downlight_zone_count
|
|
726
|
+
target_colors = [colors] * self.downlight_zone_count
|
|
715
727
|
else:
|
|
716
728
|
target_colors = list(colors)
|
|
717
729
|
else:
|
|
@@ -750,7 +762,7 @@ class CeilingLight(MatrixLight):
|
|
|
750
762
|
# Light is already on - determine target colors first, then set
|
|
751
763
|
if colors is not None:
|
|
752
764
|
if isinstance(colors, HSBK):
|
|
753
|
-
target_colors = [colors] * downlight_zone_count
|
|
765
|
+
target_colors = [colors] * self.downlight_zone_count
|
|
754
766
|
else:
|
|
755
767
|
target_colors = list(colors)
|
|
756
768
|
else:
|
|
@@ -824,6 +836,54 @@ class CeilingLight(MatrixLight):
|
|
|
824
836
|
if turning_off and self._state_file:
|
|
825
837
|
self._save_state_to_file()
|
|
826
838
|
|
|
839
|
+
async def set_color(self, color: HSBK, duration: float = 0.0) -> None:
|
|
840
|
+
"""Set light color, updating component state tracking.
|
|
841
|
+
|
|
842
|
+
Overrides Light.set_color() to track the color change in the ceiling
|
|
843
|
+
light's component state. When set_color() is called, all zones (uplight
|
|
844
|
+
and downlight) are set to the same color. This override ensures that
|
|
845
|
+
the cached component colors stay in sync so that subsequent component
|
|
846
|
+
control methods (like turn_uplight_on or turn_downlight_on) use the
|
|
847
|
+
correct color values.
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
color: HSBK color to set for the entire light
|
|
851
|
+
duration: Transition duration in seconds (default 0.0)
|
|
852
|
+
|
|
853
|
+
Raises:
|
|
854
|
+
LifxDeviceNotFoundError: If device is not connected
|
|
855
|
+
LifxTimeoutError: If device does not respond
|
|
856
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
857
|
+
|
|
858
|
+
Example:
|
|
859
|
+
```python
|
|
860
|
+
from lifx.color import HSBK
|
|
861
|
+
|
|
862
|
+
# Set entire ceiling light to warm white
|
|
863
|
+
await ceiling.set_color(
|
|
864
|
+
HSBK(hue=0, saturation=0, brightness=1.0, kelvin=2700)
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
# Later component control will use this color
|
|
868
|
+
await ceiling.turn_uplight_off() # Uplight off
|
|
869
|
+
await ceiling.turn_uplight_on() # Restores to warm white
|
|
870
|
+
```
|
|
871
|
+
"""
|
|
872
|
+
# Call parent to perform actual color change
|
|
873
|
+
await super().set_color(color, duration)
|
|
874
|
+
|
|
875
|
+
# Update cached component colors - all zones now have the same color
|
|
876
|
+
self._last_uplight_color = color
|
|
877
|
+
self._last_downlight_colors = [color] * self.downlight_zone_count
|
|
878
|
+
|
|
879
|
+
# Also update stored state for restoration
|
|
880
|
+
self._stored_uplight_state = color
|
|
881
|
+
self._stored_downlight_state = [color] * self.downlight_zone_count
|
|
882
|
+
|
|
883
|
+
# Persist if enabled
|
|
884
|
+
if self._state_file:
|
|
885
|
+
self._save_state_to_file()
|
|
886
|
+
|
|
827
887
|
async def turn_downlight_off(
|
|
828
888
|
self, colors: HSBK | list[HSBK] | None = None, duration: float = 0.0
|
|
829
889
|
) -> None:
|
|
@@ -845,8 +905,6 @@ class CeilingLight(MatrixLight):
|
|
|
845
905
|
Note:
|
|
846
906
|
Sets all downlight zone brightness to 0 on device while preserving H, S, K.
|
|
847
907
|
"""
|
|
848
|
-
expected_count = len(range(*self.downlight_zones.indices(256)))
|
|
849
|
-
|
|
850
908
|
# Validate provided colors early (before fetching)
|
|
851
909
|
stored_colors: list[HSBK] | None = None
|
|
852
910
|
if colors is not None:
|
|
@@ -856,16 +914,16 @@ class CeilingLight(MatrixLight):
|
|
|
856
914
|
"Provided color cannot have brightness=0. "
|
|
857
915
|
"Omit the parameter to use current colors."
|
|
858
916
|
)
|
|
859
|
-
stored_colors = [colors] *
|
|
917
|
+
stored_colors = [colors] * self.downlight_zone_count
|
|
860
918
|
else:
|
|
861
919
|
if all(c.brightness == 0 for c in colors):
|
|
862
920
|
raise ValueError(
|
|
863
921
|
"Provided colors cannot have brightness=0. "
|
|
864
922
|
"Omit the parameter to use current colors."
|
|
865
923
|
)
|
|
866
|
-
if len(colors) !=
|
|
924
|
+
if len(colors) != self.downlight_zone_count:
|
|
867
925
|
raise ValueError(
|
|
868
|
-
f"Expected {
|
|
926
|
+
f"Expected {self.downlight_zone_count} colors for downlight, "
|
|
869
927
|
f"got {len(colors)}"
|
|
870
928
|
)
|
|
871
929
|
stored_colors = list(colors)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""mDNS/DNS-SD discovery for LIFX devices.
|
|
2
|
+
|
|
3
|
+
This module provides mDNS-based discovery using the _lifx._udp.local service type.
|
|
4
|
+
It uses only Python stdlib (no external dependencies).
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
Low-level API (raw mDNS records):
|
|
8
|
+
```python
|
|
9
|
+
async for record in discover_lifx_services():
|
|
10
|
+
print(f"Found: {record.serial} at {record.ip}:{record.port}")
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
High-level API (device instances):
|
|
14
|
+
```python
|
|
15
|
+
async for device in discover_devices_mdns():
|
|
16
|
+
print(f"Found {type(device).__name__}: {device.serial}")
|
|
17
|
+
```
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from lifx.network.mdns.discovery import (
|
|
21
|
+
create_device_from_record,
|
|
22
|
+
discover_devices_mdns,
|
|
23
|
+
discover_lifx_services,
|
|
24
|
+
)
|
|
25
|
+
from lifx.network.mdns.dns import (
|
|
26
|
+
DnsHeader,
|
|
27
|
+
DnsResourceRecord,
|
|
28
|
+
ParsedDnsResponse,
|
|
29
|
+
SrvData,
|
|
30
|
+
TxtData,
|
|
31
|
+
build_ptr_query,
|
|
32
|
+
parse_dns_response,
|
|
33
|
+
parse_name,
|
|
34
|
+
parse_txt_record,
|
|
35
|
+
)
|
|
36
|
+
from lifx.network.mdns.transport import MdnsTransport
|
|
37
|
+
from lifx.network.mdns.types import LifxServiceRecord
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# Types
|
|
41
|
+
"LifxServiceRecord",
|
|
42
|
+
# Discovery functions
|
|
43
|
+
"discover_lifx_services",
|
|
44
|
+
"discover_devices_mdns",
|
|
45
|
+
"create_device_from_record",
|
|
46
|
+
# DNS parsing
|
|
47
|
+
"DnsHeader",
|
|
48
|
+
"DnsResourceRecord",
|
|
49
|
+
"ParsedDnsResponse",
|
|
50
|
+
"SrvData",
|
|
51
|
+
"TxtData",
|
|
52
|
+
"build_ptr_query",
|
|
53
|
+
"parse_dns_response",
|
|
54
|
+
"parse_name",
|
|
55
|
+
"parse_txt_record",
|
|
56
|
+
# Transport
|
|
57
|
+
"MdnsTransport",
|
|
58
|
+
]
|