40751fecb7
Lint & Test / test (push) Successful in 1m24s
- Cache per-entity colors in HALightTargetProcessor._update_lights()
- Broadcast colors_update to WS clients at target's update_rate
- WS endpoint: /api/v1/output-targets/{target_id}/ha-light/ws
- Frontend: connect WS when target runs, update swatch colors live
- Card shows colored boxes per mapped entity with entity name labels
270 lines
12 KiB
Python
270 lines
12 KiB
Python
"""Output target schemas (CRUD, processing state, metrics)."""
|
|
|
|
from datetime import datetime
|
|
from typing import Dict, Optional, List
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds between health checks
|
|
|
|
|
|
class KeyColorRectangleSchema(BaseModel):
|
|
"""A named rectangle for key color extraction (relative coords 0.0-1.0)."""
|
|
|
|
name: str = Field(description="Rectangle name", min_length=1, max_length=50)
|
|
x: float = Field(default=0.0, description="Left edge (0.0-1.0)", ge=0.0, le=1.0)
|
|
y: float = Field(default=0.0, description="Top edge (0.0-1.0)", ge=0.0, le=1.0)
|
|
width: float = Field(default=1.0, description="Width (0.0-1.0)", gt=0.0, le=1.0)
|
|
height: float = Field(default=1.0, description="Height (0.0-1.0)", gt=0.0, le=1.0)
|
|
|
|
|
|
class HALightMappingSchema(BaseModel):
|
|
"""Maps an LED range to one HA light entity."""
|
|
|
|
entity_id: str = Field(description="HA light entity ID (e.g. 'light.living_room')")
|
|
led_start: int = Field(default=0, ge=0, description="Start LED index (0-based)")
|
|
led_end: int = Field(default=-1, description="End LED index (-1 = last)")
|
|
brightness_scale: float = Field(
|
|
default=1.0, ge=0.0, le=1.0, description="Brightness multiplier"
|
|
)
|
|
|
|
|
|
class OutputTargetCreate(BaseModel):
|
|
"""Request to create an output target."""
|
|
|
|
name: str = Field(description="Target name", min_length=1, max_length=100)
|
|
target_type: str = Field(default="led", description="Target type (led, ha_light)")
|
|
# LED target fields
|
|
device_id: str = Field(default="", description="LED device ID")
|
|
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
|
brightness_value_source_id: str = Field(default="", description="Brightness value source ID")
|
|
fps: int = Field(default=30, ge=1, le=90, description="Target send FPS (1-90)")
|
|
keepalive_interval: float = Field(
|
|
default=1.0,
|
|
description="Keepalive send interval when screen is static (0.5-5.0s)",
|
|
ge=0.5,
|
|
le=5.0,
|
|
)
|
|
state_check_interval: int = Field(
|
|
default=DEFAULT_STATE_CHECK_INTERVAL,
|
|
description="Device health check interval (5-600s)",
|
|
ge=5,
|
|
le=600,
|
|
)
|
|
min_brightness_threshold: int = Field(
|
|
default=0,
|
|
ge=0,
|
|
le=254,
|
|
description="Min brightness threshold (0=disabled); below this → off",
|
|
)
|
|
adaptive_fps: bool = Field(
|
|
default=False, description="Auto-reduce FPS when device is unresponsive"
|
|
)
|
|
protocol: str = Field(
|
|
default="ddp",
|
|
pattern="^(ddp|http)$",
|
|
description="Send protocol: ddp (UDP) or http (JSON API)",
|
|
)
|
|
# HA light target fields
|
|
ha_source_id: str = Field(
|
|
default="", description="Home Assistant source ID (for ha_light targets)"
|
|
)
|
|
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
|
None, description="LED-to-light mappings (for ha_light targets)"
|
|
)
|
|
update_rate: float = Field(
|
|
default=2.0, ge=0.5, le=5.0, description="Service call rate in Hz (for ha_light targets)"
|
|
)
|
|
transition: float = Field(
|
|
default=0.5, ge=0.0, le=10.0, description="HA transition seconds (for ha_light targets)"
|
|
)
|
|
color_tolerance: int = Field(
|
|
default=5,
|
|
ge=0,
|
|
le=50,
|
|
description="Skip service call if RGB delta < this (for ha_light targets)",
|
|
)
|
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
|
|
|
|
|
class OutputTargetUpdate(BaseModel):
|
|
"""Request to update an output target."""
|
|
|
|
name: Optional[str] = Field(None, description="Target name", min_length=1, max_length=100)
|
|
# LED target fields
|
|
device_id: Optional[str] = Field(None, description="LED device ID")
|
|
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
|
brightness_value_source_id: Optional[str] = Field(
|
|
None, description="Brightness value source ID"
|
|
)
|
|
fps: Optional[int] = Field(None, ge=1, le=90, description="Target send FPS (1-90)")
|
|
keepalive_interval: Optional[float] = Field(
|
|
None, description="Keepalive interval (0.5-5.0s)", ge=0.5, le=5.0
|
|
)
|
|
state_check_interval: Optional[int] = Field(
|
|
None, description="Health check interval (5-600s)", ge=5, le=600
|
|
)
|
|
min_brightness_threshold: Optional[int] = Field(
|
|
None, ge=0, le=254, description="Min brightness threshold (0=disabled); below this → off"
|
|
)
|
|
adaptive_fps: Optional[bool] = Field(
|
|
None, description="Auto-reduce FPS when device is unresponsive"
|
|
)
|
|
protocol: Optional[str] = Field(
|
|
None, pattern="^(ddp|http)$", description="Send protocol: ddp (UDP) or http (JSON API)"
|
|
)
|
|
# HA light target fields
|
|
ha_source_id: Optional[str] = Field(
|
|
None, description="Home Assistant source ID (for ha_light targets)"
|
|
)
|
|
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
|
None, description="LED-to-light mappings (for ha_light targets)"
|
|
)
|
|
update_rate: Optional[float] = Field(
|
|
None, ge=0.5, le=5.0, description="Service call rate Hz (for ha_light targets)"
|
|
)
|
|
transition: Optional[float] = Field(
|
|
None, ge=0.0, le=10.0, description="HA transition seconds (for ha_light targets)"
|
|
)
|
|
color_tolerance: Optional[int] = Field(
|
|
None, ge=0, le=50, description="RGB delta tolerance (for ha_light targets)"
|
|
)
|
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
|
tags: Optional[List[str]] = None
|
|
|
|
|
|
class OutputTargetResponse(BaseModel):
|
|
"""Output target response."""
|
|
|
|
id: str = Field(description="Target ID")
|
|
name: str = Field(description="Target name")
|
|
target_type: str = Field(description="Target type")
|
|
# LED target fields
|
|
device_id: str = Field(default="", description="LED device ID")
|
|
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
|
brightness_value_source_id: str = Field(default="", description="Brightness value source ID")
|
|
fps: Optional[int] = Field(None, description="Target send FPS")
|
|
keepalive_interval: float = Field(default=1.0, description="Keepalive interval (s)")
|
|
state_check_interval: int = Field(
|
|
default=DEFAULT_STATE_CHECK_INTERVAL, description="Health check interval (s)"
|
|
)
|
|
min_brightness_threshold: int = Field(
|
|
default=0, description="Min brightness threshold (0=disabled)"
|
|
)
|
|
adaptive_fps: bool = Field(
|
|
default=False, description="Auto-reduce FPS when device is unresponsive"
|
|
)
|
|
protocol: str = Field(default="ddp", description="Send protocol (ddp or http)")
|
|
# HA light target fields
|
|
ha_source_id: str = Field(default="", description="Home Assistant source ID (ha_light)")
|
|
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
|
None, description="LED-to-light mappings (ha_light)"
|
|
)
|
|
update_rate: Optional[float] = Field(None, description="Service call rate Hz (ha_light)")
|
|
ha_transition: Optional[float] = Field(None, description="HA transition seconds (ha_light)")
|
|
color_tolerance: Optional[int] = Field(None, description="RGB delta tolerance (ha_light)")
|
|
description: Optional[str] = Field(None, description="Description")
|
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
|
created_at: datetime = Field(description="Creation timestamp")
|
|
updated_at: datetime = Field(description="Last update timestamp")
|
|
|
|
|
|
class OutputTargetListResponse(BaseModel):
|
|
"""List of output targets response."""
|
|
|
|
targets: List[OutputTargetResponse] = Field(description="List of output targets")
|
|
count: int = Field(description="Number of targets")
|
|
|
|
|
|
class TargetProcessingState(BaseModel):
|
|
"""Processing state for an output target."""
|
|
|
|
target_id: str = Field(description="Target ID")
|
|
device_id: Optional[str] = Field(None, description="Device ID")
|
|
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
|
processing: bool = Field(description="Whether processing is active")
|
|
fps_actual: Optional[float] = Field(None, description="Actual FPS achieved")
|
|
fps_potential: Optional[float] = Field(
|
|
None, description="Potential FPS (processing speed without throttle)"
|
|
)
|
|
fps_target: Optional[int] = Field(None, description="Target FPS")
|
|
frames_skipped: Optional[int] = Field(None, description="Frames skipped (no screen change)")
|
|
frames_keepalive: Optional[int] = Field(
|
|
None, description="Keepalive frames sent during standby"
|
|
)
|
|
fps_current: Optional[int] = Field(None, description="Frames sent in the last second")
|
|
timing_send_ms: Optional[float] = Field(None, description="DDP send time (ms)")
|
|
timing_extract_ms: Optional[float] = Field(
|
|
None, description="Border pixel extraction time (ms)"
|
|
)
|
|
timing_map_leds_ms: Optional[float] = Field(None, description="LED color mapping time (ms)")
|
|
timing_smooth_ms: Optional[float] = Field(None, description="Temporal smoothing time (ms)")
|
|
timing_total_ms: Optional[float] = Field(
|
|
None, description="Total processing time per frame (ms)"
|
|
)
|
|
timing_audio_read_ms: Optional[float] = Field(None, description="Audio device read time (ms)")
|
|
timing_audio_fft_ms: Optional[float] = Field(None, description="Audio FFT analysis time (ms)")
|
|
timing_audio_render_ms: Optional[float] = Field(
|
|
None, description="Audio visualization render time (ms)"
|
|
)
|
|
display_index: Optional[int] = Field(None, description="Current display index")
|
|
overlay_active: bool = Field(
|
|
default=False, description="Whether visualization overlay is active"
|
|
)
|
|
last_update: Optional[datetime] = Field(None, description="Last successful update")
|
|
errors: List[str] = Field(default_factory=list, description="Recent errors")
|
|
device_online: bool = Field(default=False, description="Whether device is reachable")
|
|
device_latency_ms: Optional[float] = Field(None, description="Health check latency in ms")
|
|
device_name: Optional[str] = Field(None, description="Device name reported by firmware")
|
|
device_version: Optional[str] = Field(None, description="Firmware version")
|
|
device_led_count: Optional[int] = Field(None, description="LED count reported by device")
|
|
device_rgbw: Optional[bool] = Field(None, description="Whether device uses RGBW LEDs")
|
|
device_led_type: Optional[str] = Field(
|
|
None, description="LED chip type (e.g. WS2812B, SK6812 RGBW)"
|
|
)
|
|
device_fps: Optional[int] = Field(
|
|
None, description="Device-reported FPS (WLED internal refresh rate)"
|
|
)
|
|
device_last_checked: Optional[datetime] = Field(None, description="Last health check time")
|
|
device_error: Optional[str] = Field(None, description="Last health check error")
|
|
device_streaming_reachable: Optional[bool] = Field(
|
|
None, description="Device reachable during streaming (HTTP probe)"
|
|
)
|
|
fps_effective: Optional[int] = Field(None, description="Effective FPS after adaptive reduction")
|
|
|
|
|
|
class TargetMetricsResponse(BaseModel):
|
|
"""Target metrics response."""
|
|
|
|
target_id: str = Field(description="Target ID")
|
|
device_id: Optional[str] = Field(None, description="Device ID")
|
|
processing: bool = Field(description="Whether processing is active")
|
|
fps_actual: Optional[float] = Field(None, description="Actual FPS")
|
|
fps_target: Optional[int] = Field(None, description="Target FPS")
|
|
uptime_seconds: float = Field(description="Processing uptime in seconds")
|
|
frames_processed: int = Field(description="Total frames processed")
|
|
errors_count: int = Field(description="Total error count")
|
|
last_error: Optional[str] = Field(None, description="Last error message")
|
|
last_update: Optional[datetime] = Field(None, description="Last update timestamp")
|
|
|
|
|
|
class BulkTargetRequest(BaseModel):
|
|
"""Request body for bulk start/stop operations."""
|
|
|
|
ids: List[str] = Field(description="List of target IDs to operate on")
|
|
|
|
|
|
class BulkTargetResponse(BaseModel):
|
|
"""Response for bulk start/stop operations."""
|
|
|
|
started: List[str] = Field(
|
|
default_factory=list, description="IDs that were successfully started"
|
|
)
|
|
stopped: List[str] = Field(
|
|
default_factory=list, description="IDs that were successfully stopped"
|
|
)
|
|
errors: Dict[str, str] = Field(
|
|
default_factory=dict, description="Map of target ID to error message for failures"
|
|
)
|