WebSocket Client API
Overview
This document outlines the enhancement of LedFx’s WebSocket connection system to support persistent client metadata tracking and client-to-client messaging capabilities. These features enable richer multi-client experiences, better observability of connected clients, and coordinated interactions between frontend instances, mobile apps, and API clients.
Please also see the dev guides:
WebSocket-Centric Architecture (v1)
Design Philosophy: This feature enhancement is built on LedFx’s WebSocket-first architecture. All client-to-client broadcasting in v1 originates from WebSocket connections.
Why WebSocket-Originated Broadcasts:
Verified Sender Identity - Sender UUID is derived directly from the authenticated WebSocket connection, eliminating spoofing risks
Architectural Consistency - Matches LedFx’s existing event-driven, real-time communication model
Simplified Authentication - No additional REST authentication layer needed; WebSocket connections are already authenticated
Bidirectional Communication - Clients can send broadcasts and receive responses over the same connection
REST Broadcast Endpoint (Future):
A REST broadcast endpoint (POST /api/clients) is NOT implemented in v1.
v1 Scope: WebSocket-only broadcasts. REST endpoint deferred to future release.
Future REST Implementation Requirements: If REST broadcasts are added in a future release, they MUST:
Be restricted to localhost (127.0.0.1, ::1) by default
Require explicit
allow_remote_broadcast: trueconfiguration to enable remote accessUse a special “system” sender identity for unauthenticated requests
Feature Requirements
Feature 1: Persistent Client Metadata
Description
Each WebSocket connection should maintain a persistent identity with metadata that survives the duration of the connection.
Metadata Fields
Field |
Type |
Description |
Source |
|---|---|---|---|
|
String |
Unique connection identifier |
Auto-generated (existing) |
|
String (optional) |
Persistent device identifier |
Client-provided |
|
String |
Human-readable client name |
Client-provided or auto-generated |
|
String (enum) |
Client category |
Client-provided with validation |
|
String |
Client IP address |
Auto-detected (existing) |
|
Timestamp |
Initial connection time |
Auto-generated |
Client Types (Enumeration)
controller- Web UI or primary control interfacevisualiser- Display-only client (e.g., fullscreen visualization)mobile- Mobile app clientdisplay- Dedicated display device (e.g., Raspberry Pi kiosk)api- Programmatic API clientnot-set- Client has not yet declared its typeunknown- Fallback for unspecified clients
Client Name Requirements
Uniqueness: Client names must be unique across all connected clients
Conflict Resolution (Initial Registration): When
set_client_infois called with a taken name, automatically append a counter:"MyClient"→"MyClient (2)"→"MyClient (3)"Client receives confirmation with
name_conflict: trueflag to indicate modificationEnsures smooth initial connection without blocking on name conflicts
Conflict Resolution (Explicit Rename): When
update_client_infois called with a taken name, reject with errorUser-initiated renames require explicit acknowledgment - no silent auto-modification
Client must choose alternative name and retry
Auto-generation: If no name provided, generate:
"Client-{first-8-chars-of-uuid}"Persistence: Name persists for the duration of the connection only
Updates: Clients can request name changes after connection via
update_client_info(subject to uniqueness check)
API Surface
WebSocket Messages (Client → Server):
// Initial metadata setup (typically sent immediately after connection)
{
id: 1, // required message correlation ID
type: "set_client_info",
data: {
device_id: "abc123-device-uuid", // optional, for recognizing returning devices
name: "Living Room Display", // optional, will auto-generate if missing
type: "display" // optional, defaults to "unknown"
}
}
// Update metadata while connected (name and type)
{
id: 2, // required message correlation ID
type: "update_client_info",
data: {
name: "Bedroom Display", // optional, update name
type: "display" // optional, update type
}
}
WebSocket Messages (Server → Client):
// Confirmation after metadata set/update
{
id: 1, // echoes request id
event_type: "client_info_updated",
client_id: "uuid-of-this-client",
name: "Living Room Display", // final name (may differ if conflict)
type: "display",
name_conflict: false // true if name was auto-modified
}
// Error response
{
id: 1, // echoes request id
success: false,
error: {
message: "Name already in use"
}
}
REST API:
GET /api/clients
Breaking Change: As of this version, GET /api/clients always returns full client metadata objects (not just IP strings).
Response:
{
"client-uuid-1": {
"ip": "192.168.1.100",
"device_id": "abc123-device-uuid",
"name": "Living Room Display",
"type": "display",
"connected_at": 1708188000.123
},
"client-uuid-2": {
"ip": "192.168.1.101",
"device_id": null,
"name": "Client-e7a3f2d1",
"type": "unknown",
"connected_at": 1708188050.789
}
}
Event Notifications
New Event: ClientsUpdatedEvent
Fired when:
A client connects or disconnects
A client’s metadata changes (name update)
Event payload: (no additional data, listeners should query /api/clients for current state)
Use cases:
Frontend displays live client list
Monitoring systems track connection changes
Integrations react to new client types appearing
Feature 2: Client-to-Client Broadcasting
Description
Enable clients to broadcast messages to other connected clients through the server, with flexible targeting options.
Security Note: All broadcasts must use server-derived sender identity to prevent impersonation. See “Sender Identity and Security Model” section below for details.
Use Cases
Visualizer Sync - Controller broadcasts preset change to all visualizer displays
Scene Coordination - One controller activates a scene that notifies other controllers to update their UI
Color Palette Sharing - User creates a color palette and broadcasts it to other connected devices
Custom Automation - API client broadcasts custom commands to specific client types
Broadcast Types (Extensible Enum)
visualiser_control- Commands to control visualization displaysscene_sync- Scene activation/state synchronizationcolor_palette- Color palette sharingcustom- Open-ended custom broadcasts
Targeting Modes
Mode |
Description |
Configuration |
Sender Behavior |
|---|---|---|---|
|
Broadcast to all connected clients |
No additional config |
Auto-excluded (prevents self-echo) |
|
Target all clients of a specific type |
|
Included if sender matches specified type |
|
Target specific clients by name |
|
Included only if sender’s name is in list |
|
Target specific clients by UUID |
|
Included only if sender’s UUID is in list |
Request Validation
Payload Size Limit: 2 KB maximum (configurable via constant)
Schema Validation: Voluptuous schema enforcement
Target Validation:
Lenient Filtering: For
mode="names"andmode="uuids", non-existent identifiers are silently filtered (broadcasts to whoever exists from the list)Fail-Closed Security: If NO targets remain after filtering, request fails with error (prevents accidental broadcasts to zero recipients)
Sender Exclusion:
mode="all": Sender is automatically excluded (prevents self-echo)mode="type": Sender is included if they match the specified typemode="names": Sender is excluded UNLESS their name is explicitly in the names listmode="uuids": Sender is excluded UNLESS their UUID is explicitly in the uuids list
Type value must be a valid client type
Request fails only if no targets match after filtering
Target Specification Validation Rules
Security Invariant: Targeting must be explicit. Invalid or ambiguous target specifications fail closed (broadcast rejected, not sent to unintended recipients).
Common Pitfall: If mode="type" with missing/empty value, naive implementations might match clients with type=None, causing unintended targeting of all clients without metadata.
Required Validation:
Mode:
"all"Broadcasts to all connected clients except sender (prevents self-echo)
No additional fields required
Ignore
value,names, oruuidsif presentAlways valid (assuming at least one other client is connected)
Mode:
"type"MUST include
valuefieldvalueMUST be a non-empty stringvalueMUST be a valid client type from the enumReject if
valueis missing, empty string, or nullError:
"Target mode 'type' requires a non-empty 'value' field"
Mode:
"names"MUST include
namesfieldnamesMUST be a non-empty listEach name MUST be a non-empty string
Reject if
namesis missing, empty list, or contains empty stringsLenient Filtering: Non-existent names are silently ignored (broadcasts to whoever exists)
Fail-Closed: If NO clients match any of the specified names, request fails
Error:
"Target mode 'names' requires a non-empty 'names' list"(schema validation)Error:
"No clients matched target specification"(zero matches after filtering)
Mode:
"uuids"MUST include
uuidsfielduuidsMUST be a non-empty listEach UUID MUST be a non-empty string
Reject if
uuidsis missing, empty list, or contains empty stringsLenient Filtering: Non-existent UUIDs are silently ignored (broadcasts to whoever exists)
Fail-Closed: If NO clients match any of the specified UUIDs, request fails
Error:
"Target mode 'uuids' requires a non-empty 'uuids' list"(schema validation)Error:
"No clients matched target specification"(zero matches after filtering) ✅ Valid Requests:
// Mode: all
{ target: { mode: "all" } }
// Mode: type
{ target: { mode: "type", value: "visualiser" } }
// Mode: names
{ target: { mode: "names", names: ["Display 1", "Display 2"] } }
// Mode: uuids
{ target: { mode: "uuids", uuids: ["abc-123", "def-456"] } }
❌ Invalid Requests (Must Reject with 400):
// Missing value for type mode
{ target: { mode: "type" } }
// Error: "Target mode 'type' requires a non-empty 'value' field"
// Empty value for type mode
{ target: { mode: "type", value: "" } }
// Error: "Target mode 'type' requires a non-empty 'value' field"
// Null value for type mode
{ target: { mode: "type", value: null } }
// Error: "Target mode 'type' requires a non-empty 'value' field"
// Missing names for names mode
{ target: { mode: "names" } }
// Error: "Target mode 'names' requires a non-empty 'names' list"
// Empty names list
{ target: { mode: "names", names: [] } }
// Error: "Target mode 'names' requires a non-empty 'names' list"
// Names list with empty string
{ target: { mode: "names", names: ["Display 1", ""] } }
// Error: "Target mode 'names' requires a non-empty 'names' list"
// Missing uuids for uuids mode
{ target: { mode: "uuids" } }
// Error: "Target mode 'uuids' requires a non-empty 'uuids' list"
// Empty uuids list
{ target: { mode: "uuids", uuids: [] } }
// Error: "Target mode 'uuids' requires a non-empty 'uuids' list"
Lenient Filtering Examples:
The lenient filtering behavior allows broadcasts to “whoever is available” from a list, which is useful for multi-device scenarios where clients may disconnect/reconnect:
// Scenario: Sender wants to broadcast to Display 1, Display 2, Display 3
// Currently connected: Display 1 (uuid-1), Display 2 (uuid-2)
// Display 3 is offline
// Request with mode="names"
{
target: { mode: "names", names: ["Display 1", "Display 2", "Display 3"] }
}
// Result: ✅ Broadcasts to Display 1 and Display 2
// "Display 3" is silently ignored (not connected)
// targets_matched: 2
// If ALL specified names are offline:
{
target: { mode: "names", names: ["Display 3", "Display 4"] }
}
// Result: ❌ Error "No clients matched target specification"
Sender Exclusion Examples:
// Scenario 1: mode="all" - Sender always excluded
// 3 clients connected (uuid-sender, uuid-1, uuid-2)
{
target: { mode: "all" }
}
// Result: ✅ Broadcasts to uuid-1 and uuid-2 only
// uuid-sender (the sender) is automatically excluded to prevent self-echo
// targets_matched: 2
// Scenario 2: mode="type" - Sender included if matching type
// Sender has type="display", 2 other displays connected
{
target: { mode: "type", value: "display" }
}
// Result: ✅ Broadcasts to all 3 displays (including sender)
// Sender is included because they match type="display"
// targets_matched: 3
// Scenario 3: mode="uuids" - Honors explicit list
// Sender is uuid-sender
{
target: { mode: "uuids", uuids: ["uuid-1", "uuid-2"] }
}
// Result: ✅ Broadcasts to uuid-1 and uuid-2
// Sender (uuid-sender) NOT in list, so excluded
// targets_matched: 2
{
target: { mode: "uuids", uuids: ["uuid-sender", "uuid-1"] }
}
// Result: ✅ Broadcasts to uuid-sender and uuid-1
// Sender (uuid-sender) IS in list, so INCLUDED (explicit opt-in)
// targets_matched: 2
// Scenario 4: mode="names" - Honors explicit list
// Sender name is "Controller-1"
{
target: { mode: "names", names: ["Display-1", "Display-2"] }
}
// Result: ✅ Broadcasts to Display-1 and Display-2
// Sender (Controller-1) NOT in list, so excluded
// targets_matched: 2
{
target: { mode: "names", names: ["Controller-1", "Display-1"] }
}
// Result: ✅ Broadcasts to Controller-1 and Display-1
// Sender (Controller-1) IS in list, so INCLUDED (explicit opt-in)
// targets_matched: 2
Test Cases:
✅
mode="all"→ broadcasts to all connected clients except sender✅
mode="type", value="visualiser"→ broadcasts to clients withtype="visualiser"including sender if sender matches type✅
mode="type", value="unknown"→ broadcasts to clients withtype="unknown"including sender if sender matches type❌
mode="type", value=""→ rejected (400 error)❌
mode="type", value=null→ rejected (400 error)❌
mode="type"(missing value) → rejected (400 error)✅
mode="type", value="display"with no matching clients → rejected (no targets matched)✅
mode="type", value="display"with clienttype=None→ client NOT targeted (explicit type required)✅
mode="names", names=["Client-1"]with sender name != “Client-1” → broadcasts to Client-1 only (sender excluded)✅
mode="names", names=["Client-1", "Sender-Name"]with sender name = “Sender-Name” → broadcasts to Client-1 and sender (explicit inclusion)✅
mode="names", names=["Client-1", "Client-999"]with only Client-1 connected → broadcasts to Client-1 (lenient)✅
mode="names", names=["Client-999"]with Client-999 offline → rejected (no targets matched)❌
mode="names", names=[]→ rejected (400 error)❌
mode="names"(missing names) → rejected (400 error)✅
mode="uuids", uuids=["abc-123"]with sender uuid != “abc-123” → broadcasts to abc-123 only (sender excluded)✅
mode="uuids", uuids=["abc-123", "sender-uuid"]with sender uuid = “sender-uuid” → broadcasts to abc-123 and sender (explicit inclusion)✅
mode="uuids", uuids=["abc-123", "xyz-999"]with only abc-123 connected → broadcasts to abc-123 (lenient)✅
mode="uuids", uuids=["xyz-999"]with xyz-999 offline → rejected (no targets matched)❌
mode="uuids", uuids=[]→ rejected (400 error)❌
mode="uuids"(missing uuids) → rejected (400 error)
Client Matching Behavior:
When mode="type", only clients with explicitly set type metadata are considered:
Client with
type="visualiser"→ matches filtervalue="visualiser"Client with
type="unknown"→ matches filtervalue="unknown"Client with
type=None(no metadata set) → does NOT match any type filterClient with
type=""(empty string, shouldn’t happen) → does NOT match any type filter
This prevents accidental broadcasts to clients that haven’t registered metadata.
API Surface
WebSocket API (v1 Implementation):
// Client sends via WebSocket (sender identity derived from connection)
{
"id": 1,
"type": "broadcast",
"data": {
"broadcast_type": "visualiser_control",
"target": {
"mode": "type",
"value": "display"
},
"payload": {
"command": "set_brightness",
"value": 80
}
}
}
Event Flow
Client sends broadcast request (via WebSocket)
Server derives sender identity from authenticated connection (never trusts client-provided sender_id)
Server validates schema and payload size
Server filters target clients based on targeting mode
If no targets match, return error
Server fires
ClientBroadcastEventwith server-derived sender fieldsServer logs broadcast with audit trail (request_id, sender, targets, type)
Server sends broadcast event to ALL subscribers of
client_broadcastevent typeClients MUST filter by checking if their UUID is in
target_uuidslist (client-side filtering)
Important: The broadcast event is sent to all clients subscribed to client_broadcast, regardless of the targeting mode. The target_uuids field is metadata that clients use for client-side filtering. This means:
All subscribers receive the event payload (including those not in
target_uuids)Clients are responsible for checking
target_uuids.includes(myClientId)before processingSensitive data in payloads is visible to all subscribers (consider this in your threat model)
Event Payload:
{
event_type: "client_broadcast",
broadcast_type: "visualiser_control",
broadcast_id: "b-abc123def456", // Server-generated unique ID
sender_uuid: "uuid-of-sender", // Server-derived (trustworthy)
sender_name: "Living Room Controller", // From metadata (may be null)
sender_type: "controller", // From metadata (may be "unknown")
target_uuids: ["uuid-1", "uuid-2", "uuid-3"],
payload: {
command: "set_brightness",
value: 80
}
}
Security Guarantee: All sender fields (sender_uuid, sender_name, sender_type) are populated by the server based on the authenticated connection, never from client-provided data.
Broadcast Delivery Architecture
Design Decision: Broadcast-to-All with Client-Side Filtering
The current implementation uses LedFx’s existing event system which broadcasts to all subscribers. This means:
Server-Side:
Server fires
ClientBroadcastEventto the event systemEvent system sends to ALL WebSocket connections subscribed to
client_broadcastThe
target_uuidslist is included as metadata in the event payload
Client-Side:
Every subscriber receives every broadcast event
Clients MUST check if their UUID is in
target_uuidsbefore processingClients SHOULD filter out their own broadcasts (check
sender_uuid)
Architectural Implications:
✅ Advantages:
Simple implementation using existing event infrastructure
Consistent with LedFx’s event-driven architecture
No need to maintain WebSocket connection registry for targeting
❌ Limitations:
Privacy: All subscribers see payload data not intended for them (visible before client-side filtering)
Efficiency: Network bandwidth used sending to clients who will discard the message
Security Audit: Harder to prove data isolation since all clients receive all payloads
Client Implementation Requirements:
All clients subscribing to client_broadcast events MUST implement this filtering pattern:
if (data.event_type === 'client_broadcast') {
// 1. REQUIRED: Check if broadcast is for us
if (!data.target_uuids.includes(myClientId)) {
return; // Not for us - discard immediately
}
// 2. OPTIONAL: Filter out own broadcasts
if (data.sender_uuid === myClientId) {
return; // We sent this - discard
}
// 3. Process the broadcast
handleBroadcast(data);
}
Sender Identity and Security Model
Security Invariant
Sender identity MUST be derived from the authenticated WebSocket connection, NEVER from client-provided data.
The server is the sole source of truth for client identity:
For WebSocket broadcasts:
sender_uuidcomes from the WebSocket connection instance (self.uid)For REST broadcasts (if implemented): Server derives identity from request context or uses “system” sender
Any
sender_idfield in a client request body MUST be rejected with an error (do not silently ignore)
Critical: sender_uuid is ALWAYS server-derived from the WebSocket connection. Client-provided sender identity fields are security vulnerabilities and must not be accepted.
Implementation Approaches
Option A: WebSocket-Only Broadcasts ✅
Add new WebSocket message type: {"type": "broadcast", ...}
How it works:
Client sends broadcast message via its existing WebSocket connection
Server uses the connection’s UUID (already authenticated) as sender identity
Server looks up sender metadata (name, type) from class-level storage
No REST endpoint needed for broadcasts
Advantages:
Inherently secure: sender = authenticated WebSocket connection
Consistent with WebSocket-first architecture
Simpler implementation (no REST auth to manage)
Real-time bidirectional communication already established
Sender Identity Resolution:
# In WebSocket handler
async def handle_broadcast(self, data):
# Derive sender identity from WebSocket connection (server-side)
sender_uuid = self.uid # Canonical client UUID
sender_name = self.client_name or f"Client-{sender_uuid[:8]}"
sender_type = self.client_type
# sender_uuid, sender_name, sender_type are now server-derived and trustworthy
Event Payload Identity Fields
All ClientBroadcastEvent payloads MUST include server-derived sender fields:
{
"sender_uuid": str, # Server-derived, never from client
"sender_name": str | None, # From metadata, fallback to "Client-{uuid[:8]}"
"sender_type": str, # From metadata, default "unknown"
}
Privacy Note:
sender_ipis intentionally excluded from broadcast events to protect client privacy. IP addresses are only available in server-side connection metadata and logs.
Fallback Behavior:
If metadata not set:
sender_name = f"Client-{sender_uuid[:8]}"If type not set:
sender_type = "unknown"If metadata lookup fails: Log error, use UUID-based fallback
Audit Logging Requirements
Every broadcast request MUST be logged with:
_LOGGER.info(
f"Broadcast {broadcast_id}: type={broadcast_type}, "
f"sender={sender_name} ({sender_uuid[:8]}), "
f"targets={len(target_uuids)} clients"
)
Log Fields:
broadcast_id: Unique identifier for correlationbroadcast_type: Envelope typesender_name: Sender metadatasender_uuid: Server-derived sender identity (truncated for readability)targets: Number of matched target clientstimestamp: Implicit in log entry
Privacy Note: The audit log intentionally omits payload contents and sender IP addresses. IP addresses are available in connection metadata (
GET /api/clients) if needed for debugging.
Security Logging:
Failed broadcasts: Log with
_LOGGER.warning()(client error)Invalid sender resolution: Log with
_LOGGER.error()(system error)Suspicious patterns: High-frequency broadcasts from single sender
Non-Functional Requirements
Performance
Scalability: Support up to 100 concurrent clients without significant performance degradation
Low Latency: Metadata operations should complete in <50ms under normal load
Broadcast Efficiency: Broadcasting to 10 clients should complete in <100ms
Concurrency & Thread Safety
Atomic Operations: Metadata updates must be atomic (no partial states visible)
Race Condition Free: Name uniqueness checks must not have TOCTOU vulnerabilities
Consistent Reads: Clients reading metadata must see consistent snapshots
Event Ordering: Events must fire only after related state changes are persisted
General Security Requirements
Rate Limiting: Consider rate limiting for:
Client metadata updates (prevent rapid name change spam)
Broadcast requests (prevent DoS via broadcast spam)
Payload Validation: All inputs must be validated against schemas
Size Limits: Enforce maximum payload sizes to prevent memory exhaustion
Authorization: Existing WebSocket authentication/authorization applies (no additional auth needed)
Reliability
Graceful Degradation: System should handle:
Malformed messages (return errors, don’t crash)
Disconnections during metadata updates
Rapid connect/disconnect cycles
Error Handling: All error conditions should be logged and reported to clients appropriately
Task Cleanup: Background tasks must be properly tracked and cleaned up on disconnect
Technical Constraints
Code Quality
Async/Await Consistency: All async operations must be properly awaited
Locking Strategy: Shared state must be protected with appropriate locks (asyncio.Lock)
Import Organization: All imports at top of file (project standard)
Type Hints: Use type hints where beneficial
Error Logging: Use appropriate log levels:
_LOGGER.warning()for expected client errors (invalid requests)_LOGGER.error()for system errors
Testing Requirements
Unit Tests:
Name conflict resolution (sequential and concurrent)
Target filtering logic for all modes
Payload validation and size limits
Integration Tests:
Concurrent client connections
Metadata persistence across handler calls
Event ordering guarantees
Broadcast delivery to correct targets
Breaking Change - GET Endpoint:
GET /api/clientsreturns metadata objectsOld format:
{ "uuid": "ip_address", ... }(simple IP map)New format:
{ "uuid": { metadata_object }, ... }(full metadata)Breaking change - frontend must update to new format
Backwards Compatibility
Existing WebSocket Clients: Clients that don’t send
set_client_infoshould continue workingAuto-generate name:
"Client-{uuid[:8]}"Default type:
"unknown"No device_id
Breaking Change - GET Endpoint:
GET /api/clientsresponse format changedOld format:
{ "uuid": "ip_address", ... }(simple IP map)New format:
{ "uuid": { "name": "...", "type": "...", "ip": "...", ... }, ... }(metadata objects)Impact: Frontend and any external integrations must update in same release
Out of Scope
The following are explicitly not included in this feature:
❌ Authentication/authorization changes (uses existing WebSocket auth)
❌ Persistent storage of client metadata between server restarts
❌ Client presence indicators (“online/offline” status display)
❌ Direct peer-to-peer messaging (all communication goes through server)
❌ Broadcast message history or replay
❌ Guaranteed message delivery or acknowledgment protocols
❌ Rate limiting (should be added separately if needed)
❌ Client permissions/roles system
❌ Broadcast encryption or signing
Out of Scope (v1 Specifically)
The following are not goals for v1 and should only be considered if specific use cases emerge:
❌ REST broadcast endpoint - v1 uses WebSocket-only broadcasts. REST endpoint (
POST /api/clientswithaction: "broadcast") deferred❌ Remote REST broadcasts (non-localhost) - If REST endpoint added later, must be localhost-only by default
❌ Unauthenticated REST broadcast support - If REST endpoint added later, use “system” sender for unauthenticated requests
These could be considered for future enhancements if use cases emerge.
Example Use Case Scenarios
Scenario 1: Multi-Room Display Setup
Setup:
Living Room: Web UI controller (laptop)
Bedroom: Display-only client (Raspberry Pi)
Kitchen: Display-only client (Tablet)
Flow:
All three clients connect to LedFx server
Living room controller sends
set_client_infowithname: "Control Center",type: "controller"Bedroom display sends
set_client_infowithname: "Bedroom Display",type: "display"Kitchen display sends
set_client_infowithname: "Kitchen Display",type: "display"User queries
GET /api/clientsand sees all three clients listed by nameUser changes a preset in living room controller
Controller broadcasts to all
"display"type clients:{ command: "reload_presets" }Both bedroom and kitchen displays receive broadcast and reload their preset list
Scenario 2: Mobile App Sync
Setup:
Desktop: Web UI (primary controller)
Phone: Mobile app (secondary controller)
Flow:
Desktop connects, sets name “Desktop Control”
Phone connects, tries to set name “Desktop Control” → auto-renamed to “Desktop Control (2)”
User activates scene “Party Mode” on desktop
Desktop broadcasts to
mode: "all":{ broadcast_type: "scene_sync", scene_id: "party_mode" }Phone receives broadcast, updates its UI to show “Party Mode” is active
User’s experience is synchronized across devices
Scenario 3: API Integration
Setup:
Web UI: Primary controller
Custom Script: Python API client monitoring state
Flow:
Web UI connects as
type: "controller"Python script connects via WebSocket, sets
type: "api",name: "State Monitor"Admin queries
GET /api/clients, sees both clients and their typesWeb UI broadcasts color palette change to
mode: "type",value: "api"Python script receives palette data and logs it to external system
Only API clients receive this broadcast, not the UI itself
References
Related PR: #1711 (initial implementation, requires revision)
WebSocket Handler Pattern: See
ledfx/api/websocket.pyREST API Patterns: See
ledfx/api/*.py, especially helpers inRestEndpointEvent System: See
ledfx/events.pyProject Coding Standards: See
.github/copilot-instructions.md