Network Layer¶
The network layer provides low-level operations for communicating with LIFX devices over UDP.
Discovery¶
Functions for discovering LIFX devices on the local network.
discover_devices
async
¶
discover_devices(
timeout: float = DISCOVERY_TIMEOUT,
broadcast_address: str = "255.255.255.255",
port: int = LIFX_UDP_PORT,
max_response_time: float = MAX_RESPONSE_TIME,
idle_timeout_multiplier: float = IDLE_TIMEOUT_MULTIPLIER,
device_timeout: float = DEFAULT_REQUEST_TIMEOUT,
max_retries: int = DEFAULT_MAX_RETRIES,
) -> AsyncGenerator[DiscoveredDevice, None]
Discover LIFX devices on the local network.
Sends a broadcast DeviceGetService packet and yields devices as they respond.
Implements DoS protection via timeout, source validation, and serial validation.
Serial validation and per-serial deduplication are enforced inside
_discover_with_packet, so every caller of that shared generator benefits.
| PARAMETER | DESCRIPTION |
|---|---|
timeout
|
Discovery timeout in seconds
TYPE:
|
broadcast_address
|
Broadcast address to use
TYPE:
|
port
|
UDP port to use (default LIFX_UDP_PORT)
TYPE:
|
max_response_time
|
Max time to wait for responses
TYPE:
|
idle_timeout_multiplier
|
Idle timeout multiplier
TYPE:
|
device_timeout
|
Request timeout set on discovered devices
TYPE:
|
max_retries
|
Max retries per request set on discovered devices
TYPE:
|
| YIELDS | DESCRIPTION |
|---|---|
AsyncGenerator[DiscoveredDevice, None]
|
DiscoveredDevice instances as they are discovered |
AsyncGenerator[DiscoveredDevice, None]
|
(deduplicated by serial number) |
Example
Source code in src/lifx/network/discovery.py
DiscoveredDevice
dataclass
¶
DiscoveredDevice(
serial: str,
ip: str,
port: int = LIFX_UDP_PORT,
timeout: float = DEFAULT_REQUEST_TIMEOUT,
max_retries: int = DEFAULT_MAX_RETRIES,
first_seen: float = time(),
response_time: float = 0.0,
)
Information about a discovered LIFX device.
| ATTRIBUTE | DESCRIPTION |
|---|---|
serial |
Device serial number as 12-digit hex string (e.g., "d073d5123456")
TYPE:
|
ip |
Device IP address
TYPE:
|
port |
Device UDP port
TYPE:
|
first_seen |
Timestamp when device was first discovered
TYPE:
|
response_time |
Response time in seconds
TYPE:
|
| METHOD | DESCRIPTION |
|---|---|
create_device |
Create appropriate device instance based on product capabilities. |
__hash__ |
Hash based on serial number for deduplication. |
__eq__ |
Equality based on serial number. |
Methods:¶
create_device
async
¶
create_device() -> Device | None
Create appropriate device instance based on product capabilities.
Queries the device for its product ID and uses the product registry to instantiate the appropriate device class (Device, Light, HevLight, InfraredLight, MultiZoneLight, MatrixLight, or CeilingLight) based on the product capabilities.
This is the single source of truth for device type detection and instantiation across the library.
| RETURNS | DESCRIPTION |
|---|---|
Device | None
|
Device instance of the appropriate type |
| RAISES | DESCRIPTION |
|---|---|
LifxDeviceNotFoundError
|
If device doesn't respond |
LifxTimeoutError
|
If device query times out |
LifxProtocolError
|
If device returns invalid data |
Example
Source code in src/lifx/network/discovery.py
__eq__
¶
DiscoveryResponse¶
Response dataclass from custom discovery broadcasts (using packets other than GetService).
DiscoveryResponse
dataclass
¶
DiscoveryResponse(
serial: str,
ip: str,
port: int,
response_time: float,
response_payload: dict[str, Any],
)
Response from a discovery broadcast using a custom packet.
| ATTRIBUTE | DESCRIPTION |
|---|---|
serial |
Device serial number
TYPE:
|
ip |
Device IP address
TYPE:
|
port |
UDP source port the device responded from (
TYPE:
|
response_time |
Response time in seconds
TYPE:
|
response_payload |
Unpacked State packet fields as key/value dict |
UDP Transport¶
Low-level UDP transport for sending and receiving LIFX protocol messages.
UdpTransport
¶
UDP transport for sending and receiving LIFX packets.
This class provides a simple interface for UDP communication with LIFX devices. It uses asyncio for async I/O operations.
| PARAMETER | DESCRIPTION |
|---|---|
port
|
Local port to bind to (0 for automatic assignment)
TYPE:
|
broadcast
|
Enable broadcast mode for device discovery
TYPE:
|
| METHOD | DESCRIPTION |
|---|---|
open |
Open the UDP socket. |
send |
Send data to a specific address. |
receive |
Receive data from socket with size validation. |
receive_many |
Receive multiple packets within timeout period. |
close |
Close the UDP socket. |
| ATTRIBUTE | DESCRIPTION |
|---|---|
is_open |
Check if socket is open.
TYPE:
|
Source code in src/lifx/network/transport.py
Attributes¶
Methods:¶
open
async
¶
Open the UDP socket.
Source code in src/lifx/network/transport.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
send
async
¶
Send data to a specific address.
| PARAMETER | DESCRIPTION |
|---|---|
data
|
Bytes to send
TYPE:
|
address
|
Tuple of (host, port) |
| RAISES | DESCRIPTION |
|---|---|
NetworkError
|
If socket is not open or send fails |
Source code in src/lifx/network/transport.py
receive
async
¶
Receive data from socket with size validation.
| PARAMETER | DESCRIPTION |
|---|---|
timeout
|
Timeout in seconds
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
tuple[bytes, tuple[str, int]]
|
Tuple of (data, address) where address is (host, port) |
| RAISES | DESCRIPTION |
|---|---|
LifxTimeoutError
|
If no data received within timeout |
NetworkError
|
If socket is not open or receive fails |
ProtocolError
|
If packet size is invalid |
Source code in src/lifx/network/transport.py
receive_many
async
¶
receive_many(
timeout: float = 5.0, max_packets: int | None = None
) -> list[tuple[bytes, tuple[str, int]]]
Receive multiple packets within timeout period.
| PARAMETER | DESCRIPTION |
|---|---|
timeout
|
Total timeout in seconds
TYPE:
|
max_packets
|
Maximum number of packets to receive (None for unlimited)
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[tuple[bytes, tuple[str, int]]]
|
List of (data, address) tuples |
| RAISES | DESCRIPTION |
|---|---|
NetworkError
|
If socket is not open |
.. deprecated:: 5.5.0
:meth:receive_many is deprecated and will be removed in v6.0.
Use :meth:receive in a loop, or the public discovery API in
:mod:lifx.api (e.g. :func:~lifx.api.discover), for
multi-response collection.
Source code in src/lifx/network/transport.py
close
async
¶
Close the UDP socket.
Source code in src/lifx/network/transport.py
Examples¶
Device Discovery¶
from lifx.network.discovery import discover_devices
async def main():
# Discover all devices on the network
devices = await discover_devices(timeout=3.0)
for device in devices:
print(f"Found: {device.label} at {device.ip}")
print(f" Serial: {device.serial}")
Concurrency¶
Request Serialization on Single Connection¶
Each DeviceConnection serializes requests using a lock to prevent response mixing:
import asyncio
from lifx.network.connection import DeviceConnection
from lifx.protocol.packets import Light, Device
async def main():
conn = DeviceConnection(serial="d073d5123456", ip="192.168.1.100")
# Sequential requests (serialized by internal lock)
state = await conn.request(Light.GetColor())
power = await conn.request(Light.GetPower())
label = await conn.request(Device.GetLabel())
# Connection automatically closes when done
await conn.close()
Concurrent Requests on Different Devices¶
import asyncio
from lifx.network.connection import DeviceConnection
async def main():
conn1 = DeviceConnection(serial="d073d5000001", ip="192.168.1.100")
conn2 = DeviceConnection(serial="d073d5000002", ip="192.168.1.101")
# Fully parallel - different UDP sockets
result1, result2 = await asyncio.gather(
conn1.request(Light.GetColor()),
conn2.request(Light.GetColor())
)
# Clean up connections
await conn1.close()
await conn2.close()
Connection Management¶
DeviceConnection
¶
DeviceConnection(
serial: str,
ip: str,
port: int = LIFX_UDP_PORT,
max_retries: int = DEFAULT_MAX_RETRIES,
timeout: float = DEFAULT_REQUEST_TIMEOUT,
)
Connection to a LIFX device.
This class manages the UDP transport and request/response lifecycle for a single device. Connections are opened lazily on first request and remain open until explicitly closed.
Features: - Lazy connection opening (no context manager required) - Async generator-based request/response streaming - Retry logic with exponential backoff and jitter - Request serialization to prevent response mixing - Automatic sequence number management
Example
With context manager (recommended for cleanup):
async with DeviceConnection(...) as conn:
state = await conn.request(packets.Light.GetColor())
# Connection automatically closed on exit
This is lightweight - doesn't actually create a connection. Connection is opened lazily on first request.
| PARAMETER | DESCRIPTION |
|---|---|
serial
|
Device serial number as 12-digit hex string (e.g., 'd073d5123456')
TYPE:
|
ip
|
Device IP address
TYPE:
|
port
|
Device UDP port (default LIFX_UDP_PORT)
TYPE:
|
max_retries
|
Maximum number of retry attempts (default: 8)
TYPE:
|
timeout
|
Default timeout for requests in seconds (default: 8.0)
TYPE:
|
| METHOD | DESCRIPTION |
|---|---|
__aenter__ |
Enter async context manager. |
__aexit__ |
Exit async context manager and close connection. |
open |
Open connection to device. |
close |
Close connection to device. |
send_packet |
Send a packet to the device. |
receive_packet |
Receive a packet from the device. |
request_stream |
Send request and yield unpacked responses. |
request |
Send request and get single response (convenience wrapper). |
| ATTRIBUTE | DESCRIPTION |
|---|---|
is_open |
Check if connection is open.
TYPE:
|
Source code in src/lifx/network/connection.py
Attributes¶
Methods:¶
__aenter__
async
¶
__aexit__
async
¶
__aexit__(
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: object,
) -> None
Exit async context manager and close connection.
open
async
¶
Open connection to device.
Opens the UDP transport for sending and receiving packets. Called automatically on first request if not already open.
Source code in src/lifx/network/connection.py
close
async
¶
Close connection to device.
Source code in src/lifx/network/connection.py
send_packet
async
¶
send_packet(
packet: Any,
source: int | None = None,
sequence: int = 0,
ack_required: bool = False,
res_required: bool = False,
) -> None
Send a packet to the device.
| PARAMETER | DESCRIPTION |
|---|---|
packet
|
Packet dataclass instance
TYPE:
|
source
|
Client source identifier (optional, allocated if None)
TYPE:
|
sequence
|
Sequence number (default: 0)
TYPE:
|
ack_required
|
Request acknowledgement
TYPE:
|
res_required
|
Request response
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ConnectionError
|
If connection is not open or send fails |
Source code in src/lifx/network/connection.py
receive_packet
async
¶
receive_packet(timeout: float = 0.5) -> tuple[LifxHeader, bytes]
Receive a packet from the device.
Note
This method does not validate the source IP address. Validation is instead performed using the LIFX protocol's built-in target field (serial number) and sequence number matching in request_stream() and request_ack_stream(). This approach is more reliable in complex network configurations (NAT, multiple interfaces, bridges, etc.) while maintaining security through proper protocol-level validation.
| PARAMETER | DESCRIPTION |
|---|---|
timeout
|
Timeout in seconds
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
tuple[LifxHeader, bytes]
|
Tuple of (header, payload) |
| RAISES | DESCRIPTION |
|---|---|
ConnectionError
|
If connection is not open |
TimeoutError
|
If no response within timeout |
Source code in src/lifx/network/connection.py
request_stream
async
¶
request_stream(
packet: Any, timeout: float | None = None
) -> AsyncGenerator[Any, None]
Send request and yield unpacked responses.
This is an async generator that handles the complete request/response cycle including packet type detection, response unpacking, and label decoding. Connection is opened automatically if not already open.
Single response (most common): async for response in conn.request_stream(GetLabel()): process(response) break # Exit immediately
Multiple responses
async for state in conn.request_stream(GetColorZones()): process(state) # Continues until timeout
| PARAMETER | DESCRIPTION |
|---|---|
packet
|
Packet instance to send
TYPE:
|
timeout
|
Request timeout in seconds
TYPE:
|
| YIELDS | DESCRIPTION |
|---|---|
AsyncGenerator[Any, None]
|
Unpacked response packet instances (including StateUnhandled if device |
AsyncGenerator[Any, None]
|
doesn't support the command) |
AsyncGenerator[Any, None]
|
For SET packets: yields True (acknowledgement) or False (StateUnhandled) |
| RAISES | DESCRIPTION |
|---|---|
LifxTimeoutError
|
If request times out |
LifxProtocolError
|
If response invalid |
LifxConnectionError
|
If connection fails |
Example
# GET request yields unpacked packets
async for state in conn.request_stream(packets.Light.GetColor()):
color = HSBK.from_protocol(state.color)
label = state.label # Already decoded to string
break
# SET request yields True (acknowledgement) or False (StateUnhandled)
async for result in conn.request_stream(
packets.Light.SetColor(color=hsbk, duration=1000)
):
if result:
# Acknowledgement received
pass
else:
# Device doesn't support this command
pass
break
# Multi-response GET - stream all responses
async for state in conn.request_stream(
packets.MultiZone.GetExtendedColorZones()
):
# Process each zone state
pass
Source code in src/lifx/network/connection.py
785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 | |
request
async
¶
Send request and get single response (convenience wrapper).
This is a convenience method that returns the first response from request_stream(). It's equivalent to: await anext(conn.request_stream(packet))
Most device operations use this method since they expect a single response. Connection is opened automatically if not already open.
| PARAMETER | DESCRIPTION |
|---|---|
packet
|
Packet instance to send
TYPE:
|
timeout
|
Request timeout in seconds
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Any
|
Single unpacked response packet (including StateUnhandled if device |
Any
|
doesn't support the command) |
Any
|
For SET packets: True (acknowledgement) or False (StateUnhandled) |
| RAISES | DESCRIPTION |
|---|---|
LifxTimeoutError
|
If no response within timeout |
LifxProtocolError
|
If response invalid |
LifxConnectionError
|
If connection fails |
Example
# GET request returns unpacked packet
state = await conn.request(packets.Light.GetColor())
color = HSBK.from_protocol(state.color)
label = state.label # Already decoded to string
# SET request returns True or False
success = await conn.request(
packets.Light.SetColor(color=hsbk, duration=1000)
)
if not success:
# Device doesn't support this command (returned StateUnhandled)
pass
Source code in src/lifx/network/connection.py
Performance Considerations¶
Connection Lifecycle¶
- Connections open lazily on first request
- Each device owns its own connection (no shared pool)
- Connections close explicitly via
close()or context manager exit - Low memory overhead (one UDP socket per device)
Response Handling¶
- Responses matched by sequence number
- Async generator-based streaming for efficient multi-response protocols
- Immediate exit for single-response requests (no wasted timeout)
- Retry logic with exponential backoff and jitter
Rate Limiting¶
The library intentionally does not implement rate limiting to keep the core library simple. Applications should implement their own rate limiting if needed. According to the LIFX protocol specification, devices can handle approximately 20 messages per second.