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.
| 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
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 | |
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. |
Functions¶
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, or MatrixLight) 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__
¶
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¶
Functions¶
open
async
¶
Open the UDP socket.
Source code in src/lifx/network/transport.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 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 | |
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 |
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¶
Functions¶
__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
760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 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 | |
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.