Troubleshooting Guide¶
Solutions to common problems when using the LIFX Emulator.
Port Conflicts and Resolution¶
Port Already in Use¶
Problem: Server fails to start with "Address already in use"
Causes:
- Another emulator instance is running
- Another application is using port 56700
- Previous test didn't clean up properly
Solutions:
-
Find what's using the port:
-
Kill the process:
-
Use dynamic port allocation (best for tests):
from lifx_emulator import EmulatedLifxServer, create_color_light from lifx_emulator.devices import DeviceManager from lifx_emulator.repositories import DeviceRepository device = create_color_light("d073d5000001") # Let the OS assign an available port server = EmulatedLifxServer( [device], DeviceManager(DeviceRepository()), "127.0.0.1", 0 ) await server.start() # server.port stays 0; the assigned port is in ipv4_endpoint print(f"Server running on port {server.ipv4_endpoint[1]}") -
Use port offset in parallel tests (pytest-xdist):
import pytest from lifx_emulator import EmulatedLifxServer, create_color_light from lifx_emulator.devices import DeviceManager from lifx_emulator.repositories import DeviceRepository @pytest.fixture async def emulator(worker_id): # worker_id is "master" without xdist, otherwise "gw0", "gw1", ... worker_num = 0 if worker_id == "master" else int(worker_id[2:]) + 1 port = 56700 + worker_num device = create_color_light(f"d073d5{worker_num:06d}") server = EmulatedLifxServer( [device], DeviceManager(DeviceRepository()), "127.0.0.1", port ) async with server: yield server -
Wait between tests:
Port Permission Denied¶
Problem: Cannot bind to port (Linux/macOS)
Cause: Ports below 1024 require root privileges
Solutions:
-
Use port >= 1024 (recommended):
-
Don't run as root (security risk)
Discovery Failures and Debugging¶
Client Cannot Find Emulated Devices¶
Problem: LIFX client library discovers nothing
Diagnostic checklist:
-
Check emulator is running:
Should show:Starting LIFX Emulator on 127.0.0.1:56700 -
Check bind address:
From the CLI, use lifx-emulator --bind 0.0.0.0, or bind the address of
one LAN interface, for example --bind 192.168.1.50. A specific address
binds 0.0.0.0 underneath so broadcasts are received, and only answers
peers on that interface's subnets; peers elsewhere are counted as
packets_filtered in /api/stats. A rising packets_filtered with no
packets_received means the client is on a different subnet: bind
0.0.0.0 instead.
-
Check firewall:
-
Windows: Allow Python through Windows Firewall
- macOS: System Preferences → Security & Privacy → Firewall → Allow Python
-
Linux:
-
Check client port:
-
Ensure client is looking on the correct port
- Default LIFX port is 56700
-
If using custom port, client must match
-
Network isolation:
-
Docker containers need
--network hostor proper port mapping - VMs need bridged networking
-
WSL2 may need port forwarding
-
Enable verbose logging:
Discovery Works, but Device Not Responding¶
Problem: Client sees device but times out on commands
Solutions:
- Check target serial:
device = create_color_light("d073d5000001")
print(f"Device serial: {device.state.serial}")
# Client must target this exact serial
-
Check tagged packets:
-
Broadcast packets have
tagged=Truein header - Unicast packets have
tagged=Falseand specific target -
Emulator routes based on target field
-
Check ack_required and res_required flags:
# Client should set these flags appropriately
# ack_required=True → Device sends acknowledgment
# res_required=True → Device sends state response
- Enable packet logging:
← RX Device.GetService from 127.0.0.1:54321 (target=broadcast, seq=1) [no payload]
→ TX Device.StateService to 127.0.0.1:54321 (target=d073d5000001, seq=1) [service=1, port=56700]
Devices Discovered Multiple Times¶
Problem: Client sees duplicate devices
Cause: Multiple emulator instances or broadcast responses
Solutions:
-
Check for multiple instances:
-
Use unique serials:
-
Check network interfaces:
- Multiple interfaces may cause duplicate broadcasts
- Bind to specific interface to avoid this
Timeout Issues¶
Client Operations Timeout¶
Problem: Client commands timeout waiting for response
Diagnostic steps:
-
Increase client timeout: Check your client library documentation for details.
-
Check response scenarios:
Scenarios live on the server's HierarchicalScenarioManager, not on the device. List what is configured at each scope:
from lifx_emulator.scenarios import get_device_type
manager = server.scenario_manager
print(manager.device_scenarios) # keyed by serial
print(manager.type_scenarios) # keyed by device type
print(manager.location_scenarios) # keyed by location label
print(manager.group_scenarios) # keyed by group label
print(manager.global_scenario)
# The merged scenario that applies to one device
print(
manager.get_scenario_for_device(
serial=device.state.serial,
device_type=get_device_type(device),
location=device.state.location_label,
group=device.state.group_label,
)
)
ScenarioConfig(drop_packets={102: 1.0}) drops every SetColor (102) request, so the client times out by design. drop_packets matches the incoming request type.
-
Check async context:
# Wrong - creating the server does not start it server = EmulatedLifxServer( [device], DeviceManager(DeviceRepository()), "127.0.0.1", 56700 ) # ... try to communicate ... # Right - the server starts on entry and stops on exit async with EmulatedLifxServer( [device], DeviceManager(DeviceRepository()), "127.0.0.1", 56700 ) as server: ... # communicate here -
Network latency:
Check whether a delay scenario is configured. response_delays is keyed by the outgoing response type, so this delays every StateColor (107), the reply to GetColor and SetColor, by 10 seconds:
Tests Timeout in CI/CD¶
Problem: Tests pass locally but timeout in CI
Solutions:
-
Use dynamic ports:
-
Increase pytest timeout (requires the
pytest-timeoutplugin): -
Widen test fixture scope:
Tests that use a module- or session-scoped server must run on the same event loop, for example withimport pytest_asyncio # Module scope for faster tests: one server per test file @pytest_asyncio.fixture(scope="module", loop_scope="module") async def emulator(): ...@pytest.mark.asyncio(loop_scope="module"). If they run on a different loop, the server's loop is idle during the test and every request times out. -
Check CI resource limits:
-
CI runners may be slower than local machine
- Increase timeouts for CI environment
- Use conditional timeouts:
Protocol Errors and Interpretation¶
Invalid Packet Type Errors¶
Problem: "Unknown packet type" errors
Cause: Client sending unsupported packet type
Solution:
-
Check protocol version:
-
Emulator supports LIFX LAN Protocol (November 2025)
-
See https://github.com/LIFX/public-protocol for specification
-
Check packet type support:
-
Enable verbose logging:
Look for:← RX Unknown packet type XXX from ...
Malformed Packet Errors¶
Problem: "Failed to unpack ..." warnings, for example Failed to unpack Light.SetColor (type 102) from 127.0.0.1:54321: ...
Causes:
- Incorrect header format
- Wrong packet structure
- Byte order issues
Solutions:
-
Check header format:
-
Header is exactly 36 bytes
- Target field is 8 bytes (6-byte serial + 2 null bytes)
-
Packet type in bytes 32-33 (little-endian)
-
Verify packet structure:
-
Enable debug logging:
Acknowledgment Not Received¶
Problem: Client expects ack but doesn't receive it
Causes:
ack_requiredflag not set in header- Request dropped by scenario configuration
- Acknowledgement delayed by scenario configuration
- Network issues
Solutions:
-
Check ack_required flag:
-
Check scenarios:
drop_packets matches the incoming request type, and a dropped request gets no acknowledgement either. response_delays matches the outgoing packet type, so it can delay the acknowledgement itself:
from lifx_emulator.scenarios import ScenarioConfig
# Drops every SetColor (102) request: no ack and no StateColor
ScenarioConfig(drop_packets={102: 1.0})
# Delays every Acknowledgement (45) by 2 seconds
ScenarioConfig(response_delays={45: 2.0})
45 in drop_packets does not suppress acknowledgements, because clients never send packet type 45.
- Enable verbose logging:
Look for:
→ TX Device.Acknowledgement to ...
Performance Problems¶
Slow Test Execution¶
Problem: Tests take too long to run
Solutions:
-
Use appropriate fixture scopes:
-
Run tests in parallel:
-
Reduce device count:
-
Remove unnecessary delays:
-
Profile your tests:
High Memory Usage¶
Problem: Emulator consuming too much memory
Causes:
- Too many devices
- Large tile/zone counts
- Memory leak in test code
Solutions:
-
Limit device count:
-
Clean up properly:
-
Check for test isolation issues:
Packet Loss Under Load¶
Problem: Packets dropped at high throughput
Causes: - OS UDP buffer limits - CPU saturation - Network congestion
Solutions:
-
Increase OS UDP buffer:
-
Reduce packet rate:
-
Check CPU usage:
Platform-Specific Issues¶
Windows Issues¶
Problem: Tests fail only on Windows
Common issues:
- Event loop policy:
No change is needed. The default Proactor event loop on Windows supports the UDP sockets the emulator uses. Remove any asyncio.set_event_loop_policy() call you added as a workaround: event loop policies are deprecated from Python 3.14.
- Firewall prompts:
- Windows Defender may prompt to allow Python
- Allow for private networks
-
Or use 127.0.0.1 binding (no firewall prompt)
-
Path separators:
-
Line endings:
- Git may convert line endings (CRLF vs LF)
- Configure
.gitattributes:
macOS Issues¶
Problem: Tests fail only on macOS
Common issues:
- Firewall blocks UDP:
- System Preferences → Security & Privacy → Firewall
- Click "Firewall Options"
-
Add Python and allow incoming connections
-
Too many open files:
-
Gatekeeper blocking Python:
Linux Issues¶
Problem: Tests fail only on Linux
Common issues:
-
Port binding requires root (ports < 1024):
-
Too many open files:
-
SELinux or AppArmor restrictions:
WSL (Windows Subsystem for Linux) Issues¶
Problem: Issues specific to WSL
Common issues:
- Port forwarding:
- WSL2 uses virtual network
- Ports may not be accessible from Windows
-
Workaround: Use
0.0.0.0binding and Windows firewall rules -
Network latency:
- WSL2 has slight network overhead
-
May need longer timeouts
-
File permissions:
- Files on Windows filesystem (e.g., /mnt/c/) have weird permissions
- Use Linux filesystem (e.g., ~/projects/)
Logging and Debugging Techniques¶
Enable Verbose Logging¶
Basic logging:
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
Emulator-specific logging:
CLI verbose mode:
Output (timestamps and field lists trimmed):
← RX Device.GetService from 127.0.0.1:54321 (target=broadcast, seq=1) [no payload]
→ TX Device.StateService to 127.0.0.1:54321 (target=d073d5000001, seq=1) [service=1, port=56700]
← RX Light.GetColor from 127.0.0.1:54321 (target=d073d5000001, seq=2) [no payload]
→ TX Light.StateColor to 127.0.0.1:54321 (target=d073d5000001, seq=2) [color=..., power=..., label=...]
Inspect Device State¶
During tests:
import pytest
from lifx_emulator import EmulatedLifxServer, create_color_light
from lifx_emulator.devices import DeviceManager
from lifx_emulator.repositories import DeviceRepository
@pytest.fixture
async def emulator():
device = create_color_light("d073d5000001")
server = EmulatedLifxServer(
[device], DeviceManager(DeviceRepository()), "127.0.0.1", 0
)
async with server:
yield server, device # Expose device for inspection
async def test_color_change(emulator):
server, device = emulator
# ... send SetColor command ...
# Inspect device state
print(f"Device color: {device.state.color}")
print(f"Device power: {device.state.power_level}")
assert device.state.color.hue == expected_hue
Packet Capture with Wireshark/tcpdump¶
Capture UDP packets:
# Linux/macOS - tcpdump
sudo tcpdump -i lo -n udp port 56700 -X
# Wireshark
# Filter: udp.port == 56700
Analyze LIFX packets: - First 36 bytes: LIFX header - Bytes 32-33: Packet type (little-endian) - Remaining bytes: Payload
Use pytest Debugging¶
Drop into debugger on failure:
Drop into debugger on first failure:
Set breakpoint in test:
async def test_something(emulator):
device = ...
breakpoint() # Python 3.7+
# or
import pdb; pdb.set_trace()
Enable pytest output:**¶
Common Error Messages¶
"TypeError: 'NoneType' object is not subscriptable" (or requests silently time out)¶
Cause: Attempting to use the server before starting it. Creating an EmulatedLifxServer does not bind any sockets, so server.ipv4_endpoint is None and nothing answers requests.
Fix:
from lifx_emulator import EmulatedLifxServer
from lifx_emulator.devices import DeviceManager
from lifx_emulator.repositories import DeviceRepository
device_manager = DeviceManager(DeviceRepository())
# Wrong
server = EmulatedLifxServer(
[device], DeviceManager(DeviceRepository()), "127.0.0.1", 0
)
port = server.ipv4_endpoint[1] # TypeError: server not started!
# Right
server = EmulatedLifxServer(
[device], DeviceManager(DeviceRepository()), "127.0.0.1", 0
)
async with server:
port = server.ipv4_endpoint[1] # Server is running
await send_command()
"ValueError: Serial must be exactly 12 ASCII hexadecimal characters"¶
Cause: Serial not 12 hex characters
Fix:
# Wrong
device = create_color_light("123")
device = create_color_light("d073d500001") # 11 chars
# Right
device = create_color_light("d073d5000001") # 12 chars
"TypeError: Object of type 'bytes' is not JSON serializable"¶
Cause: Trying to serialize device state with bytes
Context: Usually happens with custom serialization
Fix:
# Convert bytes fields (location_id, group_id, mac_address) to hex strings
location_id = device.state.location_id.hex()
"SyntaxError: 'await' outside async function"¶
Cause: Using async operations outside async context
Fix:
import asyncio
# Right
async def test_device():
await server.start() # Now it works
# Or use asyncio.run() outside pytest
def main():
asyncio.run(async_main())
"DeprecationWarning: There is no current event loop"¶
Cause: Calling asyncio.get_event_loop() when no event loop is running. Python 3.10 and later deprecate this, and Python 3.14 raises RuntimeError instead.
Fix:
- Inside a coroutine, use
asyncio.get_running_loop(). - Outside one, start the loop with
asyncio.run(main())instead of fetching a loop yourself. - In pytest, write
async deftests and fixtures and let pytest-asyncio manage the loop. Don't override theevent_loopfixture: pytest-asyncio 1.0 removed it. To share a server across tests, useloop_scopeinstead:
# In conftest.py
import pytest_asyncio
from lifx_emulator import EmulatedLifxServer, create_color_light
from lifx_emulator.devices import DeviceManager
from lifx_emulator.repositories import DeviceRepository
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def emulator():
device = create_color_light("d073d5000001")
server = EmulatedLifxServer(
[device], DeviceManager(DeviceRepository()), "127.0.0.1", 0
)
async with server:
yield server
Tests that use this fixture need @pytest.mark.asyncio(loop_scope="session") so they run on the same loop as the server.
Getting Help¶
If you're still stuck after trying these solutions:
- Check existing issues: https://github.com/Djelibeybi/lifx-emulator/issues
- Search documentation: Use the search feature in the docs
- Ask in discussions: https://github.com/Djelibeybi/lifx-emulator/discussions
- File a bug report: https://github.com/Djelibeybi/lifx-emulator/issues/new
When reporting issues, include:
- Python version (python --version)
- Operating system (Linux/macOS/Windows, version)
- lifx-emulator version (pip show lifx-emulator)
- Minimal reproduction code
- Error messages and stack traces
- What you've already tried
See Also¶
- FAQ - Common questions and answers
- Best Practices - Patterns and anti-patterns
- Integration Testing - pytest patterns
- Glossary - Terminology reference