# Effects Architecture

This document provides a comprehensive overview of the Light Effects Framework architecture, including design decisions, implementation details, and lifecycle management.

## Table of Contents

- [High-Level Overview](#high-level-overview)
- [Component Architecture](#component-architecture)
- [Effect Lifecycle](#effect-lifecycle)
- [State Management](#state-management)
- [Concurrency Model](#concurrency-model)
- [Device Type Handling](#device-type-handling)
- [Design Decisions](#design-decisions)

## High-Level Overview

The Light Effects Framework is built on a layered architecture that separates concerns and provides a clean abstraction for effect management:

```
┌─────────────────────────────────────────────────────┐
│              Application Layer                       │
│  (User code, business logic, effect selection)      │
└────────────────┬────────────────────────────────────┘
                 │
┌────────────────▼────────────────────────────────────┐
│              Effects API Layer                       │
│   • Conductor (orchestration + dynamic lights)      │
│   • EffectPulse (firmware waveforms via Device)     │
│   • FrameEffect → Colorloop, Rainbow, Flame,       │
│     Aurora, Progress, Sunrise/Sunset (frame-based)  │
│   • EffectRegistry (discovery + filtering)          │
│   • LIFXEffect (base class)                         │
└──────┬─────────────────────────────┬────────────────┘
       │ (EffectPulse)               │ (FrameEffect)
┌──────▼──────────────────┐  ┌───────▼───────────────────┐
│  Device Layer            │  │  Animation Layer           │
│  (lifx.devices)          │  │  (lifx.animation)          │
│  • set_waveform()        │  │  • Animator (direct UDP)   │
│  • set_color()           │  │  • PacketGenerator         │
└──────┬──────────────────┘  │  • FrameBuffer              │
       │                      └───────┬───────────────────┘
       │                              │
┌──────▼──────────────────────────────▼───────────────┐
│           Network / Protocol Layers                  │
│   • UDP transport, binary serialization             │
└─────────────────────────────────────────────────────┘
```

### Key Principles

1. **Zero Dependencies**: Uses only Python stdlib and existing lifx-async components
2. **State Preservation**: Automatically captures and restores device state
3. **Type Safety**: Full type hints with strict Pyright validation
4. **Async/Await**: Native asyncio for concurrent operations
5. **Extensibility**: Abstract base class for custom effects

## Component Architecture

### Module Structure

```
src/lifx/effects/
├── __init__.py              # Public API exports
├── base.py                  # LIFXEffect abstract base class
├── frame_effect.py          # FrameEffect base class + FrameContext
├── conductor.py             # Conductor orchestrator
├── pulse.py                 # EffectPulse implementation
├── colorloop.py             # EffectColorloop (FrameEffect) implementation
├── rainbow.py               # EffectRainbow (FrameEffect) implementation
├── flame.py                 # EffectFlame (FrameEffect) implementation
├── aurora.py                # EffectAurora (FrameEffect) implementation
├── progress.py              # EffectProgress (FrameEffect, multizone only)
├── sunrise.py               # EffectSunrise + EffectSunset (FrameEffect, matrix only)
├── registry.py              # EffectRegistry, DeviceType, DeviceSupport, EffectInfo
├── models.py                # PreState, RunningEffect dataclasses
├── state_manager.py         # DeviceStateManager for state capture/restore
└── const.py                 # Effect constants
```

### Component Responsibilities

#### Conductor (`conductor.py`)

**Purpose:** Central orchestrator managing effect lifecycle across multiple devices.

**Responsibilities:**

- Track running effects per device (serial → RunningEffect mapping)
- Capture device state before effects
- Power on devices if needed
- Execute effects via `effect.async_perform()`
- Restore device state after effects complete
- Handle concurrent effects on different devices
- Provide thread-safe state management with `asyncio.Lock()`

**Key Data Structures:**

```python
class Conductor:
    _running: dict[str, RunningEffect]  # serial → RunningEffect
    _lock: asyncio.Lock                 # Thread-safe access
```

#### LIFXEffect Base Class (`base.py`)

**Purpose:** Abstract base for all effect implementations.

**Responsibilities:**

- Define effect interface (abstract `async_play()` method)
- Handle power-on logic in `async_perform()`
- Provide startup color via `from_poweroff_hsbk()`
- Enable state inheritance optimization via `inherit_prestate()`
- Store conductor reference and participants

**Key Methods:**

```python
class LIFXEffect(ABC):
    async def async_perform(participants):  # Setup + call async_play()
        ...

    @abstractmethod
    async def async_play():                 # Effect logic (override)
        ...

    async def from_poweroff_hsbk(light):    # Startup color (override)
        ...

    def inherit_prestate(other):            # State inheritance (override)
        ...
```

#### FrameEffect Base Class (`frame_effect.py`)

**Purpose:** Abstract base for effects that generate color frames at a fixed FPS.

**Responsibilities:**

- Define frame generation interface (abstract `generate_frame()` method)
- Run frame loop with timing control (fps, duration, stop_event)
- Create `FrameContext` per device per frame with timing and layout info
- Convert user-friendly HSBK colors to protocol tuples
- Dispatch frames via Animators (set by Conductor)
- Provide `async_setup()` hook for pre-loop initialization

**Key Data:**

```python
class FrameEffect(LIFXEffect):
    _fps: float                    # Frames per second
    _duration: float | None        # Run time limit (None = infinite)
    _stop_event: asyncio.Event     # Stop signal
    _animators: list[Animator]     # Set by Conductor before play
```

**Relationship to LIFXEffect:** `FrameEffect` extends `LIFXEffect` and implements `async_play()` as a frame loop. Subclasses implement `generate_frame()` instead.

#### Effect Implementations

**EffectPulse (`pulse.py`):**

- Extends `LIFXEffect` directly
- Implements pulse/blink/breathe effects via firmware waveforms
- Five modes with different timing and waveforms
- Intelligent color selection based on mode
- Auto-completion after configured cycles

**EffectColorloop (`colorloop.py`):**

- Extends `FrameEffect` (not LIFXEffect directly)
- Implements continuous hue rotation via frame generation
- Time-based hue calculation with configurable FPS
- Randomized direction and saturation
- Runs indefinitely until stopped
- Supports state inheritance for seamless transitions
- Works across all device types (Light, MultiZoneLight, MatrixLight)

**EffectRainbow (`rainbow.py`):**

- Extends `FrameEffect`
- Spreads full 360-degree rainbow across all pixels, scrolling over time
- Configurable period, brightness, saturation, and inter-device spread
- Best on multizone strips and matrix lights

**EffectFlame (`flame.py`):**

- Extends `FrameEffect`
- Layered sine waves produce organic flicker (no random state)
- Warm color range: hue 0-40, high saturation, configurable kelvin range
- Matrix enhancement: vertical brightness falloff (bottom rows hotter)
- Works on all color device types

**EffectAurora (`aurora.py`):**

- Extends `FrameEffect`
- Palette interpolation with shortest-path hue wrapping
- Sine wave brightness modulation creates flowing "curtain" bands
- Matrix enhancement: vertical gradient (brightest in middle rows)
- Configurable palette, speed, brightness, and inter-device spread

**EffectProgress (`progress.py`):**

- Extends `FrameEffect`
- Animated progress bar with traveling bright spot
- Mutable `position` attribute for real-time updates
- Supports single color or gradient foreground
- Multizone only (`has_multizone` required)

**EffectSunrise + EffectSunset (`sunrise.py`):**

- Both extend `FrameEffect`
- Duration-based (finite) — complete automatically
- Shared `_sun_frame()` helper: radial model centered at configurable origin
- Five color phases: night → dawn → golden hour → morning → daylight
- `origin` parameter: `"bottom"` (rectangular tiles) or `"center"` (Ceiling lights)
- Sunrise: `restore_on_complete=False` (stays at daylight)
- Sunset: optionally powers off lights after last frame
- Matrix only (`has_matrix` required)

**EffectRegistry (`registry.py`):**

- Central discovery mechanism for effect compatibility
- `DeviceType` enum: LIGHT, MULTIZONE, MATRIX
- `DeviceSupport` enum: RECOMMENDED, COMPATIBLE, NOT_SUPPORTED
- `EffectInfo` frozen dataclass with name, class, description, support map
- `get_effect_registry()` returns lazily-initialized default registry with all built-in effects

#### Data Models (`models.py`)

**PreState:**

Stores device state before effect:

```python
@dataclass
class PreState:
    power: bool                     # Power state (on/off)
    color: HSBK                     # Current color
    zone_colors: list[HSBK] | None  # Multizone colors (if applicable)
```

**RunningEffect:**

Associates effect with its pre-state:

```python
@dataclass
class RunningEffect:
    effect: LIFXEffect    # Effect instance
    prestate: PreState    # Captured state
```

## Effect Lifecycle

The effect lifecycle consists of five distinct phases:

### 1. Initialization

User creates effect instance with desired parameters:

```python
effect = EffectPulse(mode='blink', cycles=5)
```

**What happens:**

- Effect object created with parameters stored
- No network activity yet
- No conductor association yet

### 2. State Capture

Conductor starts effect and captures current device state:

```python
await conductor.start(effect, [light1, light2])
```

**What happens:**

```
For each light:
  1. Check if prestate can be inherited from running effect
  2. If not, capture new prestate:
     a. Get power state (get_power)
     b. Get current color (get_color)
     c. Get zone colors if multizone (get_color_zones or get_extended_color_zones)
  3. Store in RunningEffect and register in conductor._running
```

**Timing:** <1 second per device (mostly network I/O)

**Special Cases:**

- **Prestate Inheritance:** If `effect.inherit_prestate(current_effect)` returns `True`, reuses existing PreState
- **Multizone Devices:** Uses extended messages if supported, falls back to standard messages
- **Powered-off Devices:** All state is still captured (including zone colors that may be inaccurate)

### 3. Animator Creation (FrameEffect Only)

For `FrameEffect` subclasses, the Conductor creates Animators before the frame loop:

```python
if isinstance(effect, FrameEffect):
    animators = await self._create_animators(effect, participants)
    effect._animators = animators
    await effect.async_setup(participants)
```

**What happens:**

```
For each participant:
  1. Detect device type (MatrixLight, MultiZoneLight, Light)
  2. Create appropriate Animator factory:
     - MatrixLight → Animator.for_matrix(device, duration_ms)
     - MultiZoneLight → Animator.for_multizone(device, duration_ms)
     - Light → Animator.for_light(device, duration_ms)
  3. duration_ms = int(1000 / effect.fps) for smooth interpolation
  4. Call effect.async_setup(participants) for pre-loop initialization
```

**Note:** Animators use direct UDP — no device connection needed after creation.

### 4. Power-On (Optional)

If `effect.power_on == True`, devices are powered on:

```python
async def async_perform(self, participants):
    if self.power_on:
        for light in self.participants:
            power_level = await light.get_power()
            if power_level == 0:
                startup_color = await self.from_poweroff_hsbk(light)
                await light.set_color(startup_color, duration=0)
                await light.set_power(True, duration=0.3)
```

**What happens:**

```
For each powered-off light:
  1. Get startup color from from_poweroff_hsbk()
  2. Set color immediately (duration=0)
  3. Power on with 0.3s fade (duration=0.3)
```

**Timing:** 0.3 seconds per powered-off device

### 5. Effect Execution

Effect logic runs via `async_play()`:

```python
await effect.async_play()
```

**What happens:**

- **LIFXEffect subclasses:** Custom effect logic using device methods (e.g., `set_waveform()`)
- **FrameEffect subclasses:** Frame loop calling `generate_frame()` per device per frame, dispatching via Animators

**Timing:**

- EffectPulse: `period * cycles` seconds
- EffectColorloop: Runs indefinitely at calculated FPS

### 6. State Restoration & Cleanup

Conductor restores devices to pre-effect state:

```python
await conductor.stop([light1, light2])
```

**What happens:**

```
For each light:
  1. Restore multizone colors (if applicable):
     - Use extended messages if supported
     - Use standard messages with apply=NO_APPLY, then apply=APPLY
     - Wait 0.3s for device processing
  2. Restore color:
     - set_color(prestate.color, duration=0)
     - Wait 0.3s
  3. Restore power:
     - set_power(prestate.power, duration=0)
```

**Timing:** 0.6-1.0 seconds per device (includes delays)

**Special Cases:**

- Multizone devices get zones restored first
- 0.3s delays ensure device processing completes
- Errors are logged but don't stop other devices

## State Management

### State Storage

The conductor maintains a registry of running effects:

```python
_running: dict[str, RunningEffect]
```

**Key:** Device serial number (12-digit hex string)
**Value:** `RunningEffect` containing effect instance and captured `PreState`

### State Capture Details

#### Power State

Integer power level captured via `get_power()`:

```python
power_level = await light.get_power()  # Returns int (0 or 65535)
is_on = power_level > 0
```

#### Color State

HSBK color captured via `get_color()`:

```python
color, power, _ = await light.get_color()  # power is int (0 or 65535)
```

Returns:

- `color`: HSBK (hue, saturation, brightness, kelvin)
- `power`: Power level as integer (0 for off, 65535 for on)
- `label`: Device label

#### Multizone State

For `MultiZoneLight` devices, zone colors are captured:

```python
if isinstance(light, MultiZoneLight):
    # Get all zones using the convenience method
    # Automatically uses the best method based on capabilities
    zone_colors = await light.get_all_color_zones()
```

**Extended Multizone:**

- Single message retrieves all zones
- Returns `list[HSBK]` with all zone colors
- More efficient, used when available

**Standard Multizone:**

- Retrieves zones in batches of 8
- Multiple messages required for >8 zones
- Used as fallback for older devices

### State Restoration Details

#### Multizone Restoration

Zones are restored **before** color and power:

**Extended Multizone:**

```python
await light.set_extended_color_zones(
    zone_index=0,
    colors=zone_colors,
    duration=0.0,
    apply=MultiZoneExtendedApplicationRequest.APPLY
)
```

Single message restores all zones.

**Standard Multizone:**

```python
for i, color in enumerate(zone_colors):
    is_last = (i == len(zone_colors) - 1)
    apply = APPLY if is_last else NO_APPLY

    await light.set_color_zones(
        start=i, end=i,
        color=color,
        duration=0.0,
        apply=apply
    )
```

Multiple messages with `apply` logic:

- `NO_APPLY` (0): Update buffer, don't display
- `APPLY` (1): Update buffer and display (used on last zone)

This ensures atomic update visible only when all zones are set.

#### Timing Delays

Critical 0.3-second delays ensure device processing:

```python
# After multizone restoration
await asyncio.sleep(0.3)

# After color restoration
await asyncio.sleep(0.3)

# No delay after power (last operation)
```

Without these delays, subsequent operations may arrive before device finishes processing, causing state corruption.

### Prestate Inheritance

Optimization that skips state capture/restore for compatible consecutive effects:

```python
def inherit_prestate(self, other: LIFXEffect) -> bool:
    """Return True if can skip restoration."""
    return isinstance(other, EffectColorloop)  # Example
```

**When used:**

```python
current_running = self._running.get(serial)
if current_running and effect.inherit_prestate(current_running.effect):
    # Reuse existing prestate
    prestate = current_running.prestate
else:
    # Capture new prestate
    prestate = await self._capture_prestate(light)
```

**Benefits:**

- Eliminates flash/reset between compatible effects
- Reduces network traffic
- Faster effect transitions

**Used by:**

- `EffectColorloop.inherit_prestate()` → `True` for other `EffectColorloop`
- `EffectRainbow.inherit_prestate()` → `True` for other `EffectRainbow`
- `EffectFlame.inherit_prestate()` → `True` for other `EffectFlame`
- `EffectAurora.inherit_prestate()` → `True` for other `EffectAurora`
- `EffectProgress.inherit_prestate()` → `True` for other `EffectProgress`
- `EffectSunrise.inherit_prestate()` → `True` for other `EffectSunrise`
- `EffectSunset.inherit_prestate()` → `True` for other `EffectSunset`
- `EffectPulse` doesn't use it (returns `False`)

## Concurrency Model

### Thread Safety

The conductor uses an `asyncio.Lock()` for thread-safe state management:

```python
async def start(self, effect, participants):
    async with self._lock:
        # Critical section: state capture and registration
        for light in participants:
            prestate = await self._capture_prestate(light)
            self._running[light.serial] = RunningEffect(effect, prestate)

    # Effect execution happens outside lock (concurrent)
    await effect.async_perform(participants)
```

**Why lock is needed:**

- Prevents race conditions when starting/stopping effects concurrently
- Protects `_running` dictionary modifications
- Ensures atomic state capture and registration

**Why effect execution is outside lock:**

- Allows multiple effects to run concurrently on different devices
- Effect logic doesn't modify conductor state
- Prevents blocking other operations during long-running effects

### Concurrent Device Operations

Effects use `asyncio.gather()` for concurrent device operations:

```python
# Apply waveform to all devices concurrently
tasks = [
    light.set_waveform(color, period, cycles, waveform)
    for light in self.participants
]
await asyncio.gather(*tasks)
```

**Benefits:**

- Multiple devices updated in parallel
- Network latency overlaps
- Total time ≈ single device time (not N × device time)

### Background Response Dispatcher

Each `DeviceConnection` has a background receiver task that routes responses:

```python
# In DeviceConnection
async def _response_receiver(self):
    while self._running:
        packet = await self._receive_packet()
        # Route by sequence number to waiting coroutine
        self._pending[seq_num].set_result(packet)
```

**Implications for effects:**

- Multiple concurrent requests on same device are supported
- Responses are correctly matched even with concurrent operations
- No additional coordination needed in effect code

### Effect Concurrency Patterns

#### Pattern 1: Sequential Effects on Same Devices

```python
# Effect 1 completes before Effect 2 starts
await conductor.start(effect1, lights)
await asyncio.sleep(duration1)
await conductor.start(effect2, lights)  # Captures new state
```

State is automatically restored between effects.

#### Pattern 2: Concurrent Effects on Different Devices

```python
# Different devices, completely independent
await conductor.start(effect1, group1)
await conductor.start(effect2, group2)
# Both run concurrently
```

No locking needed - different devices, different state.

#### Pattern 3: Replacing Running Effect

```python
# Start effect1
await conductor.start(effect1, lights)
await asyncio.sleep(5)

# Replace with effect2
await conductor.start(effect2, lights)  # Prestate may be inherited
```

If `effect2.inherit_prestate(effect1)` returns `True`, no restoration happens.

## Device Type Handling

### Device Capabilities Detection

The effects framework adapts to device capabilities automatically:

```python
# Check if multizone
if isinstance(light, MultiZoneLight):
    # Capture zone colors
    zone_colors = await light.get_color_zones(...)

# Check if extended multizone supported
if light.capabilities and light.capabilities.has_extended_multizone:
    # Use efficient extended messages
    await light.set_extended_color_zones(...)
```

### Device-Specific Behavior

#### Color Lights (`Light`)

- Full HSBK color support
- All effect parameters apply
- No special handling needed

#### Multizone Lights (`MultiZoneLight`)

- **State Capture:** Zone colors captured using extended or standard messages
- **State Restoration:** All zones restored with proper `apply` logic
- **Effect Behavior:** Entire device pulses/cycles together (zones not individually controlled)
- **Timing:** 0.3s delay after zone restoration

#### Matrix Lights (`MatrixLight`)

- **FrameEffect support:** Full 2D canvas via `FrameContext.canvas_width` / `canvas_height`
- **Spatial effects:** Flame (vertical gradient), Aurora (vertical brightness), Sunrise/Sunset (radial wavefront)
- **Canvas mapping:** Multi-tile devices get a unified canvas based on tile positions

#### HEV Lights (`HevLight`)

- Treated like standard color lights
- HEV cycle not affected by effects
- Effects don't interfere with HEV functionality

#### Infrared Lights (`InfraredLight`)

- Treated like standard color lights
- Infrared LED not affected by color effects
- Effects only control visible light

#### Monochrome/White Lights

- **Color parameters ignored:** Hue and saturation have no effect
- **Brightness works:** Effects can still toggle/fade brightness
- **Kelvin preserved:** Temperature setting maintained
- **Recommendation:** Limited usefulness (only brightness changes visible)

## Design Decisions

### Why Conductor Pattern?

**Decision:** Central conductor manages all effect lifecycle instead of effects managing themselves.

**Rationale:**

1. **Centralized State:** Single source of truth for what's running where
2. **Consistent State Management:** All effects get same capture/restore logic
3. **Concurrency Control:** Single lock protects all state modifications
4. **User Simplicity:** Users don't manage state manually

**Alternative Considered:** Effects self-manage state

**Rejected because:** Would require each effect to duplicate state logic, higher chance of bugs

### Why Abstract Base Class?

**Decision:** `LIFXEffect` is abstract with required `async_play()` override.

**Rationale:**

1. **Type Safety:** Enforces effect interface at type-check time
2. **Code Reuse:** Common setup logic in `async_perform()`
3. **Extensibility:** Users can create custom effects easily
4. **Consistency:** All effects follow same pattern

### Why Two-Phase Effect Execution?

**Decision:** `async_perform()` calls `async_play()` instead of single method.

**Rationale:**

1. **Separation of Concerns:** Setup logic separate from effect logic
2. **User Simplicity:** Users only override `async_play()`, setup is automatic
3. **Consistency:** All effects get same power-on behavior
4. **Flexibility:** Base class can add more setup steps without breaking subclasses

### Why No Rate Limiting?

**Decision:** Effects don't implement rate limiting.

**Rationale:**

1. **Simplicity:** Keeps core library simple and focused
2. **Flexibility:** Applications have different rate limit needs
3. **Transparency:** Users see actual device behavior
4. **Consistency:** Matches lifx-async philosophy (no hidden rate limiting)

**Recommendation:** Applications should implement rate limiting if sending many concurrent requests.

### Why 0.3-Second Delays?

**Decision:** Fixed 0.3-second delays between state restoration operations.

**Rationale:**

1. **Device Processing Time:** LIFX devices need time to process commands
2. **Empirical Testing:** 0.3s works reliably across all device types
3. **State Integrity:** Prevents race conditions and state corruption
4. **Trade-off:** Slightly slower restoration but guaranteed correctness

**Alternative Considered:** No delays, faster restoration

**Rejected because:** Causes state corruption and unpredictable behavior

### Why Prestate Inheritance?

**Decision:** Optional optimization via `inherit_prestate()` method.

**Rationale:**

1. **Performance:** Eliminates unnecessary state reset
2. **User Experience:** No visible flash between compatible effects
3. **Opt-in:** Only used when effect explicitly enables it
4. **Safe Default:** Returns `False` unless overridden

**Use Cases:**

- ColorLoop → ColorLoop: Seamless transition
- Pulse → Pulse: Could enable but currently disabled
- Different types: Should not inherit (different visual intent)

### Why FrameEffect Pattern?

**Decision:** Separate frame generation from frame delivery via `FrameEffect` + animation module.

**Rationale:**

1. **Device Agnostic:** Effects work across all device types (Light, MultiZoneLight, MatrixLight) without device-specific code
2. **Clean Separation:** Effect authors implement `generate_frame()` returning HSBK colors; animation module handles packet construction, tile mapping, and UDP delivery
3. **Spatial Awareness:** `FrameContext` provides `pixel_count`, `canvas_width`, `canvas_height` — enabling 2D effects (fire, rain) on matrix devices
4. **Performance:** Direct UDP via prebaked packet templates, no connection overhead
5. **Smooth Transitions:** `duration_ms` parameter tells firmware to interpolate between frames

**Alternative Considered:** Keep all effects using device methods (`set_color()`, `set_color_zones()`)

**Rejected because:** Required device-specific branching in every effect, couldn't leverage the animation module's performance, and made spatial effects impractical.

## Integration Points

### With Device Layer

**LIFXEffect subclasses** (e.g., EffectPulse) use standard device methods:

- `get_power()`, `set_power()` (state capture/restore, power-on)
- `get_color()`, `set_color()` (state capture/restore)
- `set_waveform()` (EffectPulse firmware waveforms)
- `get_color_zones()`, `set_color_zones()` (MultiZoneLight state)
- `get_extended_color_zones()`, `set_extended_color_zones()` (MultiZoneLight state)

### With Animation Layer

**FrameEffect subclasses** (e.g., EffectColorloop) use the animation module for frame delivery:

- `Animator.for_light()` — single-light SetColor packets
- `Animator.for_multizone()` — multi-zone SetExtendedColorZones packets
- `Animator.for_matrix()` — multi-tile Set64 packets with canvas mapping
- `animator.send_frame()` — synchronous frame dispatch (no async overhead)
- `duration_ms` parameter — firmware-level interpolation between frames

The Conductor creates and manages Animator lifecycle (creation in `start()`, cleanup in `stop()`).

### With Network Layer

State capture/restore relies on existing lazy connection and concurrent request support:

- Lazy connections open on first request and are reused
- Requests are serialized via lock to prevent response mixing
- FrameEffect frame delivery bypasses connections (direct UDP via Animator)

### With Protocol Layer

Effects use existing protocol structures:

- `HSBK` for color representation + `HSBK.as_tuple()` for protocol conversion
- `LightWaveform` enum for waveform types (EffectPulse)
- `MultiZoneApplicationRequest` for zone apply logic (state restore)
- Auto-generated packet classes

## Performance Characteristics

### Memory Usage

- **Conductor:** ~10KB base + running effects
- **Per Effect:** ~1KB per device + effect-specific state
- **PreState:** ~200 bytes per device (~100 bytes + zone colors)

### CPU Usage

- **Minimal:** Async I/O bound, not CPU bound
- **Concurrency:** Multiple devices don't increase CPU significantly
- **Background Tasks:** Requests serialized per connection, concurrent across devices

### Network Traffic

#### State Capture

- Power: 1 request per device
- Color: 1 request per device
- Multizone: 1 request (extended) or N/8 requests (standard)
- **Total:** 3-4 packets per device

#### Effect Execution

- Pulse: 1 waveform packet per device (via device connection)
- FrameEffect (e.g., ColorLoop): 1 frame per device per FPS tick (via direct UDP Animator)

#### State Restoration

- Multizone: 1 request (extended) or N requests (standard)
- Color: 1 request per device
- Power: 1 request per device
- **Total:** 2-3 packets per device (or N+2 for standard multizone)

### Scalability

- **Tested:** 10+ devices
- **Expected:** 50+ devices in production
- **Limitation:** Network capacity and device response time
- **Recommendation:** For 50+ devices, consider grouping effects or staggering start times

## See Also

- [Getting Started](../getting-started/effects.md) - Basic usage
- [Effects Reference](../api/effects.md) - Detailed API documentation
- [Custom Effects](../user-guide/effects-custom.md) - Creating your own effects
- [Troubleshooting](../user-guide/effects-troubleshooting.md) - Common issues
