Effects Troubleshooting¶
This guide helps you diagnose and resolve common issues when using the Light Effects Framework.
Table of Contents¶
- Common Issues
- Device Compatibility
- Performance Issues
- State Management
- Debugging Techniques
- Known Limitations
Common Issues¶
Effects Don't Start¶
Symptom: Calling conductor.start() doesn't appear to do anything.
Possible Causes:
- No await keyword
# Wrong - missing await
conductor.start(effect, lights) # Returns immediately, nothing happens
# Correct
await conductor.start(effect, lights)
- Devices not reachable
# Check device connectivity first
from lifx import discover, DeviceGroup
devices = []
async for device in discover():
devices.append(device)
group = DeviceGroup(devices)
if not group.lights:
print("No devices found!")
return
# Now safe to use effects
conductor = Conductor()
await conductor.start(effect, group.lights)
- Empty participants list
# Check you have lights
if not lights:
print("No lights to apply effect to")
return
await conductor.start(effect, lights)
Solution: Always use await and verify devices are discovered before starting effects.
Lights Don't Restore to Original State¶
Symptom: After effect completes, lights stay in effect state instead of returning to original.
Possible Causes:
- Missing conductor.stop() call
# ColorLoop requires manual stop
effect = EffectColorloop(period=30)
await conductor.start(effect, lights)
await asyncio.sleep(60)
# MISSING: await conductor.stop(lights)
Solution: Always call conductor.stop() for continuous effects:
- Effect doesn't call conductor.stop() internally
Custom effects must restore state:
async def async_play(self) -> None:
# Effect logic
...
# Required for auto-restore
if self.conductor:
await self.conductor.stop(self.participants)
- Network timeout during restoration
If restoration fails due to network issues, lights may be in inconsistent state.
Solution: Check logs for timeout errors, verify network connectivity.
Effect Appears to Freeze¶
Symptom: Effect starts but never completes, script hangs.
Possible Causes:
- ColorLoop running indefinitely
ColorLoop is designed to run forever:
# This will hang forever
effect = EffectColorloop(period=30)
await conductor.start(effect, lights)
# Script hangs here - ColorLoop never completes
Solution: Call conductor.stop() explicitly:
effect = EffectColorloop(period=30)
await conductor.start(effect, lights)
await asyncio.sleep(60) # Let it run
await conductor.stop(lights) # Stop it
- Custom effect with infinite loop
Solution: Add stop condition:
async def async_play(self) -> None:
self._running = True
while self._running:
await self._do_something()
- Missing await in effect logic
async def async_play(self) -> None:
# Missing await - blocks event loop
light.set_color(color) # Should be: await light.set_color(color)
Solution: Always use await on async operations.
Lights Flash/Reset Between Effects¶
Symptom: When starting a second effect, lights briefly return to their original state before the new effect starts.
Cause: The first effect was stopped before the second started.
Solution: Start the second effect without stopping the first. The new effect replaces the running one with no restore in between, and inherits its original prior state, so stopping it later restores what was there before either effect.
Pulse Effect Too Fast/Slow¶
Symptom: Pulse timing doesn't match expectations.
Cause: Misunderstanding period vs. total duration.
# This runs for 1 second total (period=1.0, cycles=1)
effect = EffectPulse(mode='blink', period=1.0, cycles=1)
# This runs for 5 seconds total (period=1.0, cycles=5)
effect = EffectPulse(mode='blink', period=1.0, cycles=5)
# This runs for 2 seconds total (period=2.0, cycles=1)
effect = EffectPulse(mode='blink', period=2.0, cycles=1)
Solution: Total duration = period * cycles
ColorLoop Colors Look Wrong¶
Symptom: ColorLoop shows unexpected colors or is too dim/bright.
Possible Causes:
- Saturation constraints too restrictive
# Very low saturation = washed out colors
effect = EffectColorloop(saturation_min=0.1, saturation_max=0.3) # Pastels
Solution: Use higher saturation for vibrant colors:
- Brightness locked to low value
Solution: Use higher brightness or None to preserve original:
effect = EffectColorloop(brightness=None) # Preserve original
# or
effect = EffectColorloop(brightness=0.8) # 80% brightness
- Monochrome device
ColorLoop doesn't work on monochrome/white-only lights.
Solution: Only use ColorLoop on color-capable devices.
Multizone Lights Don't Restore Zones Correctly¶
Symptom: After effect, multizone light zones are wrong color or all same color.
Possible Causes:
- Device was powered off before effect
Some older multizone devices report all zones as the same color when powered off.
Workaround: Ensure lights are powered on before starting effects:
# Power on first
for light in lights:
await light.set_power(True)
await asyncio.sleep(0.5)
# Now start effect
await conductor.start(effect, lights)
- Extended multizone messages not supported
Older devices may not support efficient extended multizone messages.
Solution: Framework automatically falls back to standard messages. No action needed.
- Network timeouts during zone restoration
If restoring many zones times out, state may be incomplete.
Solution: Check network stability, reduce concurrent operations.
Device Compatibility¶
Color Lights¶
Full Support: All effects work as expected.
Models: LIFX Color, LIFX+, LIFX Mini Color, LIFX Candle Color
Monochrome/White Lights¶
Limited Support: Only brightness-based effects work.
What Works:
- EffectPulse: Brightness pulsing (hue/saturation ignored)
- Custom effects using only brightness
What Doesn't Work:
- EffectColorloop: No visible effect (can't change hue)
- Color-based custom effects: Only brightness changes visible
Recommendation: Avoid ColorLoop on monochrome devices.
Models: LIFX White, LIFX Mini White, LIFX Downlight
Multizone Lights¶
Full Support via animation module.
Works Well:
- EffectPulse: sends one waveform to all zones, which the light runs itself
- EffectColorloop: Entire device cycles color, one
SetColorper step - Custom
FrameEffect: Per-zone control viagenerate_frame()withpixel_count = zone_count
Special Considerations:
- EffectPulse applies uniformly (not per-zone)
FrameEffectsubclasses can generate individual zone colors- Zones are properly restored after effect
Models: LIFX Z, LIFX Beam
Matrix Lights (Tile/Candle/Path)¶
Full Support via animation module.
Works Well:
- EffectPulse: sends one waveform to all tiles, which the light runs itself
- EffectColorloop: All pixels cycle color together, one
SetColorper step - Custom
FrameEffect: Per-pixel control viagenerate_frame()withpixel_count = W * H
Special Considerations:
FrameContextprovidescanvas_widthandcanvas_heightfor 2D-aware effects- Multi-tile canvas mapping is automatic (tile orientation correction included)
- EffectPulse applies uniformly (not per-pixel)
Models: LIFX Tile, LIFX Candle, LIFX Path
HEV Lights¶
Full Support (treated like standard color lights).
Note: Effects don't interfere with HEV cycle functionality.
Models: LIFX Clean
Infrared Lights¶
Full Support (treated like standard color lights).
Note: Effects control visible light only, infrared LED not affected.
Models: LIFX+, LIFX Night Vision
Performance Issues¶
Slow Effect Startup¶
Symptom: Noticeable delay before effect starts.
Cause: State capture requires network round trips.
Expected Timing:
- Single device: <1 second
- 10 devices: <1 second (concurrent)
- 50 devices: 1-2 seconds
If Slower:
- Check network latency (ping devices)
- Verify devices are on local network (not remote)
- Reduce concurrent discovery operations
Choppy/Stuttering Effects¶
Symptom: Effects don't run smoothly, visible stuttering.
Possible Causes:
- Too many concurrent effects
# 50 devices all running independent effects
for light in lights:
await conductor.start(effect, [light]) # Too many!
Solution: Group devices:
- Network congestion
Too many packets sent too quickly can overwhelm network or devices.
Solution: Add rate limiting:
# In custom effect
for iteration in range(self.iterations):
await self._update_colors()
await asyncio.sleep(0.05) # Rate limit: max 20/sec
- Blocking operations in effect
# Bad - blocking sleep
import time
time.sleep(1) # Blocks entire event loop!
# Good - async sleep
await asyncio.sleep(1)
Solution: Always use async operations.
Effects on Many Devices Are Slow¶
Symptom: Effects take much longer with many devices.
Expected Behavior: Effects should scale linearly (not exponentially).
If Slower Than Expected:
- Verify concurrent operations are used:
# Good - concurrent
await asyncio.gather(*[
light.set_color(color) for light in lights
])
# Bad - sequential
for light in lights:
await light.set_color(color)
-
Check for sequential operations in custom effects
-
Verify network capacity isn't saturated
Recommendation: For 50+ devices, consider:
- Staggering effect starts
- Using fewer concurrent effects
- Implementing application-level rate limiting
State Management¶
State Captured Incorrectly¶
Symptom: Restored state doesn't match original state.
Possible Causes:
- State changed between capture and effect
# State captured here
await conductor.start(effect, lights)
# Meanwhile, user changes light with app
# Effect completes, restores OLD state (not current state)
Solution: Effects framework works correctly - this is expected behavior. State is captured at effect start.
- Multizone device powered off during capture
Older devices report inaccurate zone colors when off.
Workaround: Power on before effect:
for light in lights:
await light.set_power(True)
await asyncio.sleep(0.5)
await conductor.start(effect, lights)
State Restoration Fails Silently¶
Symptom: State restoration errors not visible.
Cause: Errors are logged but don't raise exceptions (by design - one failed device shouldn't stop others).
Solution: Enable debug logging:
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('lifx.effects')
logger.setLevel(logging.DEBUG)
Check logs for warnings like:
Debugging Techniques¶
Enable Debug Logging¶
See detailed information about effect execution:
import logging
# Enable debug logging for effects
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Just effects module
logger = logging.getLogger('lifx.effects')
logger.setLevel(logging.DEBUG)
Output shows:
- State capture details
- Prestate inheritance decisions
- State restoration steps
- Error messages
Check Current Effect Status¶
See what's currently running on each device:
conductor = Conductor()
# After starting effects
for light in lights:
current = conductor.effect(light)
if current:
print(f"{light.label}: {type(current).__name__}")
else:
print(f"{light.label}: idle")
Verify Device Connectivity¶
Before effects, verify all devices are reachable:
async def check_connectivity(lights):
"""Verify all lights respond."""
for light in lights:
try:
label = await light.get_label()
print(f"✓ {label} reachable")
except Exception as e:
print(f"✗ {light.serial} unreachable: {e}")
# Use before effects
from lifx import discover, DeviceGroup
devices = []
async for device in discover():
devices.append(device)
group = DeviceGroup(devices)
await check_connectivity(group.lights)
Test with Single Device First¶
Isolate issues by testing with one device:
# Test with single device first
from lifx import discover, DeviceGroup
devices = []
async for device in discover():
devices.append(device)
group = DeviceGroup(devices)
if group.lights:
test_light = group.lights[0]
conductor = Conductor()
effect = EffectPulse(mode='blink', cycles=3)
print(f"Testing with {await test_light.get_label()}")
await conductor.start(effect, [test_light])
await asyncio.sleep(4)
print("Test complete - check if light restored correctly")
Validate Effect Parameters¶
Check that effect parameters are valid:
# Add parameter validation
class MyEffect(LIFXEffect):
def __init__(self, count: int, period: float, power_on: bool = True):
super().__init__(power_on=power_on)
if count < 1:
raise ValueError(f"count must be positive, got {count}")
if period <= 0:
raise ValueError(f"period must be positive, got {period}")
self.count = count
self.period = period
Measure Effect Timing¶
Verify effect runs for expected duration:
import time
start = time.time()
effect = EffectPulse(mode='blink', period=1.0, cycles=5)
await conductor.start(effect, lights)
# Expected: 5 seconds
await asyncio.sleep(6)
elapsed = time.time() - start
print(f"Effect took {elapsed:.1f}s (expected ~5s)")
Known Limitations¶
Rate Limiting¶
The effects framework does not implement automatic rate limiting.
Impact: Sending too many concurrent commands may overwhelm devices or network.
LIFX Limit: ~20 messages per second per device
Recommendation: For rapid-fire effects, add your own rate limiting:
async def async_play(self) -> None:
for i in range(100):
await self._update_lights()
await asyncio.sleep(0.05) # 20/sec max
Per-Pixel Effects on Matrix and Multizone Devices¶
FrameEffect subclasses have full per-pixel control via the animation module. The FrameContext provides pixel_count, canvas_width, canvas_height and wraps, and generate_frame() returns individual colours for each pixel.
Matrix devices: generate_frame() receives pixel_count = W * H (e.g., 64 for a single 8x8 tile, 320 for 5 tiles). Pixels are in row-major order across the canvas.
Multizone devices: generate_frame() receives pixel_count = N (number of zones). Each color maps to one zone.
Example: Per-pixel rainbow using FrameEffect:
from lifx import FrameEffect, FrameContext, HSBK
class PixelRainbow(FrameEffect):
"""Rainbow across all pixels on any device type."""
@property
def name(self) -> str:
return "pixel_rainbow"
def generate_frame(self, ctx: FrameContext) -> list[HSBK]:
colors = []
for i in range(ctx.pixel_count):
hue = (ctx.elapsed_s * 30 + i * 360 / ctx.pixel_count) % 360
colors.append(HSBK(hue=hue, saturation=1.0, brightness=0.8, kelvin=3500))
return colors
Note: EffectPulse still treats all device types as single units (sends one waveform per participant). For per-pixel pulse effects, use a FrameEffect instead.
Button/Relay/Switch Devices¶
The effects framework only supports lighting devices.
Not Supported:
- LIFX Switch
- LIFX Relay
- Button devices
Reason: Effects are designed for visual output (lights), not control devices.
Network Timeouts with Many Devices¶
With 50+ devices, state capture/restoration may timeout.
Symptoms:
- Some devices don't restore state
- Timeout errors in logs
Solutions:
- Increase timeout values (requires lifx-async modification)
- Reduce number of concurrent effects
- Group devices and stagger effect starts
- Verify network infrastructure can handle traffic
Prestate Inheritance Limitations¶
State inheritance is conservative to prevent artifacts.
Current Behavior:
- Only
EffectColorloopsupports inheritance (from otherEffectColorloop) - Other effect types always reset state
Enhancement Opportunity: More effect types could support inheritance with careful design.
Still Having Issues?¶
If you're experiencing issues not covered here:
- Check the logs with debug logging enabled
- Test with single device to isolate the problem
- Verify device firmware is up to date
- Check network connectivity and stability
- Review examples in the
examples/directory - Report issues on GitHub Issues
When reporting issues, include:
- lifx-async version
- Python version
- Device model(s) affected
- Minimal reproduction code
- Full error message and traceback
- Debug logs if applicable
See Also¶
- Getting Started - Basic usage patterns
- Effects Reference - Detailed API documentation
- Custom Effects - Creating your own effects
- Architecture - How the system works