Troubleshooting Guide¶
Common issues and solutions when working with lifx.
Table of Contents¶
Discovery Issues¶
See the Discovery Guide troubleshooting section
for mDNS-specific and IPv6 zone issues. The general UDP broadcast issues below
apply to discover() and discover_udp() alike.
A device found and then lost after a power interruption is not necessarily broken: the Discovery Guide's Limitations section measures how long a device can stay undiscoverable after power returns.
No Devices Found¶
Symptom: discover() returns an empty group
Common Causes:
-
Devices not on same network
-
Firewall blocking UDP port 56700
-
Broadcast address incorrect
Try different broadcast addresses:
from lifx import discover, DeviceGroup
# Default (255.255.255.255)
devices = []
async for device in discover():
devices.append(device)
group = DeviceGroup(devices)
# Network-specific (e.g., 192.168.1.255)
devices = []
async for device in discover(broadcast_address="192.168.1.255"):
devices.append(device)
group = DeviceGroup(devices)
Solution:
import asyncio
from lifx.network.discovery import discover_devices
async def diagnose_discovery():
print("Attempting discovery...")
# Try with an extended discovery window (the default is 15.0 seconds)
devices = []
async for device in discover_devices(timeout=30.0):
devices.append(device)
if not devices:
print("No devices found. Check:")
print("1. Devices are powered on")
print("2. Devices are on the same network")
print("3. Firewall allows UDP port 56700")
print("4. Try a network-specific broadcast address")
else:
print(f"Found {len(devices)} devices:")
for device in devices:
print(f" - {device.serial} at {device.ip}")
asyncio.run(diagnose_discovery())
Partial Device Discovery¶
Symptom: Only some devices discovered
Causes:
- Devices on different subnets
- Network congestion
- Devices slow to respond
Solution:
A single discover_devices() call already re-broadcasts GetService on an
escalating schedule within the discovery window, so devices that miss the
first broadcast get several more chances to respond. There is no need to call
discovery multiple times.
If some devices are still missed on a slow or congested network, increase the
timeout to widen the discovery window:
Connection Problems¶
Connection Refused¶
Symptom: LifxConnectionError: Connection refused
Causes:
- Incorrect IP address
- Device powered off
- Network unreachable
Solution:
from lifx import Device, LifxConnectionError
import asyncio
async def test_connection(ip: str):
try:
async with await Device.connect(ip) as light:
label = await light.get_label()
print(f"Connected to: {label}")
return True
except LifxConnectionError as e:
print(f"Connection failed: {e}")
print("Check:")
print("1. Device IP is correct")
print("2. Device is powered on")
print("3. Device is reachable (try ping)")
return False
# Test connectivity
asyncio.run(test_connection("192.168.1.100"))
Connection Drops¶
Symptom: Intermittent LifxConnectionError or LifxNetworkError
Causes:
- WiFi signal weak
- Network congestion
- Device overloaded
Solution:
The library itself retransmits within each request's timeout, so transient packet loss is handled for you. An application-level wrapper like the one below is for retrying whole operations that failed — not for per-packet reliability.
import asyncio
from lifx import Device, LifxError
async def resilient_operation(ip: str, max_retries: int = 3):
"""Retry operations with exponential backoff"""
async with await Device.connect(ip) as light:
for attempt in range(max_retries):
try:
await light.set_power(True)
print("Success!")
return
except LifxError as e:
wait_time = 2 ** attempt # 1s, 2s, 4s
print(f"Attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
print(f"Retrying in {wait_time}s...")
await asyncio.sleep(wait_time)
print("All retries exhausted")
Timeout Errors¶
Request Timeouts¶
Symptom: LifxTimeoutError: Request timed out after X seconds
Causes:
- Device slow to respond
- Network latency high
- Device busy processing other requests
Solution:
The default request deadline already spans eight retries over 16 seconds, so a timeout usually means the device is unreachable rather than slow. Confirm it answers a single request at all before tuning anything:
from lifx import Device
async with await Device.connect(ip) as light:
# get_color() returns (color, power, label)
color, power, label = await light.get_color()
Discovery Timeout Too Short¶
Symptom: Some devices not found
Solution:
from lifx import discover
# Increase the discovery timeout (the default is 15.0 seconds)
devices = []
async for device in discover(timeout=30.0):
devices.append(device)
print(f"Found {len(devices)} devices")
Performance Issues¶
Slow Operations¶
Symptom: Operations take longer than expected
Diagnosis:
import asyncio
import time
from lifx import Device
async def measure_latency():
async with await Device.connect("192.168.1.100") as light:
# Measure single request
start = time.time()
await light.get_label()
elapsed = time.time() - start
print(f"Single request: {elapsed*1000:.2f}ms")
# Measure sequential requests
start = time.time()
for _ in range(10):
await light.get_label()
elapsed = time.time() - start
print(f"10 sequential: {elapsed*1000:.2f}ms ({elapsed*100:.2f}ms avg)")
# Measure concurrent requests
start = time.time()
await asyncio.gather(*[light.get_label() for _ in range(10)])
elapsed = time.time() - start
print(f"10 concurrent: {elapsed*1000:.2f}ms")
Common Causes:
- Sequential instead of concurrent operations
Slow approach (sequential):
Fast approach (concurrent):
- Not reusing connections
Inefficient (creates new connection each time):
for i in range(10):
async with await Device.connect(ip) as light:
await light.set_color(HSBK(hue=(360 / 10) * i, saturation=1.0, brightness=1.0, kelvin=3500))
Efficient (reuses connection):
async with await Device.connect(ip) as light:
for i in range(10):
await light.set_color(HSBK(hue=(360 / 10) * i, saturation=1.0, brightness=1.0, kelvin=3500))
- Need fresh data?
Use get_*() methods to always fetch from the device:
# Always fetch fresh data
# get_color() returns all three values in one call
color, power, label = await light.get_color()
# Or fetch other device info
version = await light.get_version()
Gen4 Power-Save Wake Tail¶
Symptom: The first command after a device has been idle for roughly a minute or more is slower than usual — up to ~250 ms instead of the single-digit milliseconds a busy device answers in. Subsequent commands respond at full speed.
Causes:
- Gen4 devices use WiFi power-save while idle, so the radio takes a moment to wake for the first packet
- Affects gen4 devices only — gen2 and gen3 devices show no wake tail
This is a latency effect, not a reliability problem: on healthy networks, an idle device loses zero packets. Every command still succeeds; the first one after idle just takes a little longer.
Identifying gen4 devices:
Gen4 devices report a host firmware major version of 4 or later:
firmware = await device.get_host_firmware()
if firmware.version_major >= 4:
print("Gen4 device: expect a sub-250 ms wake tail after idle")
Inside async with, the cached device.host_firmware property is already
populated, so you can check device.host_firmware.version_major directly. Do
not try to identify gen4 devices by product ID — the products registry has no
generation field.
Solution:
Most applications can ignore the wake tail entirely. If your application is latency-sensitive and cannot tolerate a slower first command, an optional periodic poll keeps the device's radio awake:
import asyncio
from lifx import Light
async def keep_awake(light: Light) -> None:
"""Optional: poll periodically so a gen4 device's radio stays awake."""
while True:
# Any request works; get_color() returns colour, power and label
# in a single request/response pair.
await light.get_color()
await asyncio.sleep(15) # 10-15 s keeps the wake tail away
lifx-async supports Python 3.10, where asyncio.TaskGroup (added in 3.11) is
unavailable, so run the poll alongside your application with
asyncio.create_task() instead:
poll_task = asyncio.create_task(keep_awake(light))
try:
await run_your_application(light)
finally:
poll_task.cancel()
try:
await poll_task
except asyncio.CancelledError:
pass
Keep a reference to the task the way poll_task does above — an unreferenced
task can be garbage-collected mid-run. Cancel it and await the cancellation in
a finally block so the poll stops cleanly on every exit path and any
exception it raised is observed rather than silently dropped as a "Task
exception was never retrieved" warning. No extra coordination beyond that is
needed, because the library serialises requests per connection. The poll is
read-only, so it is safe to run continuously, and one request every 15
seconds stays far below the ~20 msg/sec a device can handle.
Controlling several devices at once follows the same 3.10-compatible pattern
with asyncio.gather() — see
Multi-Device Control for a worked
example.
lifx-async deliberately ships no keepalive daemon
Measured on real hardware, idle devices lose zero packets on healthy networks — the wake tail is a small, bounded latency cost, not a reliability problem. Whether to spend a packet every 10–15 seconds to avoid it is the application's choice, so the library does not make it for you.
Streaming frames to a device? See the Animation Guide for how sustained streaming interacts with gen4 power-save.
Docker / Container Networking¶
Symptom: Discovery doesn't work in Docker container
Cause: Container network isolation
Solution:
Or use manual device specification:
# Don't rely on discovery
from lifx import Colors, Device
async with await Device.connect("192.168.1.100") as light:
await light.set_color(Colors.BLUE)
Debugging Tips¶
Enable Debug Logging¶
The library follows the standard library's convention for logging in libraries: it
attaches a logging.NullHandler() to the top-level lifx logger and never configures
handlers or levels itself. Nothing is written to stderr unless your application
configures logging, and any configuration you apply is respected.
import logging
# Enable DEBUG logging for lifx
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Or for specific modules
logging.getLogger('lifx.network').setLevel(logging.DEBUG)
logging.getLogger('lifx.devices').setLevel(logging.DEBUG)
Debug logging in a downstream application¶
Most applications already configure logging for themselves and only want the extra
detail from lifx-async while chasing a problem. Because the library never touches
handlers or levels, you can turn on DEBUG for the lifx logger alone and leave the
rest of your application at its normal level:
import asyncio
import logging
from lifx import discover
# Your application's normal logging configuration. Everything not covered by a
# more specific logger below is reported at INFO and above.
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
)
# Turn on DEBUG for the whole lifx-async package. The level is inherited by every
# lifx.* logger, so this covers discovery, connections, devices and effects.
logging.getLogger("lifx").setLevel(logging.DEBUG)
# Quieten a chatty part of the library while keeping DEBUG for the rest. The
# transport logger records every packet, so drop it back to INFO unless you are
# investigating wire-level behaviour.
logging.getLogger("lifx.network.transport").setLevel(logging.INFO)
logger = logging.getLogger(__name__)
async def main() -> None:
async for device in discover():
logger.info("Found %s at %s", device.serial, device.ip)
if __name__ == "__main__":
asyncio.run(main())
If you would rather keep the library's debug output out of your application's log,
give the lifx logger its own handler and stop records propagating to the root
logger:
import logging
lifx_logger = logging.getLogger("lifx")
lifx_logger.setLevel(logging.DEBUG)
lifx_logger.propagate = False
handler = logging.FileHandler("lifx-debug.log")
handler.setFormatter(
logging.Formatter("%(asctime)s %(levelname)-8s %(name)s: %(message)s")
)
lifx_logger.addHandler(handler)
Debug output from lifx.network.connection and lifx.network.transport includes
device serials and IP addresses, so treat the resulting log as private before sharing
it in a bug report.
Check Product Registry¶
from lifx.products import get_product, get_registry
# Check how many products the registry knows
registry = get_registry()
print(f"Registry contains {len(registry)} products")
# Check specific product
product = get_product(27) # LIFX A19
if product:
print(f"Name: {product.name}")
print(f"Capabilities: {product.capabilities}")
Verify Device Reachability¶
# Ping device
ping 192.168.1.100
# Check UDP port (requires nmap)
sudo nmap -sU -p 56700 192.168.1.100
# Test with netcat
echo -n "test" | nc -u 192.168.1.100 56700
Getting Help¶
If you're still experiencing issues:
- Check GitHub Issues: github.com/Djelibeybi/lifx-async/issues
- Enable debug logging: Capture logs with
logging.DEBUG - Provide details:
- Python version
- lifx version
- Device model and firmware version
- Network configuration
- Minimal reproduction code
- Full error traceback
Common Error Messages¶
| Error | Meaning | Solution |
|---|---|---|
LifxTimeoutError |
Device didn't respond | Increase timeout, check network |
LifxConnectionError |
Can't connect to device | Check IP, firewall, device power |
LifxDeviceNotFoundError |
Device not discovered | Check network, increase timeout |
LifxProtocolError |
Invalid response | Update firmware, check device type |
LifxUnsupportedCommandError |
Device doesn't support command | Check device capabilities |
AttributeError: 'Light' has no attribute 'set_color_zones' |
Wrong device class | Use MultiZoneLight |
Next Steps¶
- Discovery Guide — Discovery methods, limitations and mDNS/IPv6 troubleshooting
- Effects Troubleshooting — Issues specific to the effects framework
- Advanced Usage — Optimisation patterns
- API Reference — Complete API documentation
- FAQ — Frequently asked questions