Introduce Picture Targets to separate processing from devices
Add PictureTarget entity that bridges PictureSource to output device, separating processing settings from device connection/calibration state. This enables future target types (Art-Net, E1.31) and cleanly decouples "what to stream" from "where to stream." - Add PictureTarget/WledPictureTarget dataclasses and storage - Split ProcessorManager into DeviceState (health) + TargetState (processing) - Add /api/v1/picture-targets endpoints (CRUD, start/stop, settings, metrics) - Simplify device API (remove processing/settings/metrics endpoints) - Auto-migrate existing device settings to picture targets on first startup - Add Targets tab to WebUI with target cards and editor modal - Add en/ru locale keys for targets UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,14 +17,21 @@ from .devices import (
|
||||
Calibration,
|
||||
CalibrationTestModeRequest,
|
||||
CalibrationTestModeResponse,
|
||||
ColorCorrection,
|
||||
DeviceCreate,
|
||||
DeviceListResponse,
|
||||
DeviceResponse,
|
||||
DeviceStateResponse,
|
||||
DeviceUpdate,
|
||||
MetricsResponse,
|
||||
)
|
||||
from .picture_targets import (
|
||||
ColorCorrection,
|
||||
PictureTargetCreate,
|
||||
PictureTargetListResponse,
|
||||
PictureTargetResponse,
|
||||
PictureTargetUpdate,
|
||||
ProcessingSettings,
|
||||
ProcessingState,
|
||||
TargetMetricsResponse,
|
||||
TargetProcessingState,
|
||||
)
|
||||
from .templates import (
|
||||
EngineInfo,
|
||||
@@ -72,14 +79,19 @@ __all__ = [
|
||||
"Calibration",
|
||||
"CalibrationTestModeRequest",
|
||||
"CalibrationTestModeResponse",
|
||||
"ColorCorrection",
|
||||
"DeviceCreate",
|
||||
"DeviceListResponse",
|
||||
"DeviceResponse",
|
||||
"DeviceStateResponse",
|
||||
"DeviceUpdate",
|
||||
"MetricsResponse",
|
||||
"ColorCorrection",
|
||||
"PictureTargetCreate",
|
||||
"PictureTargetListResponse",
|
||||
"PictureTargetResponse",
|
||||
"PictureTargetUpdate",
|
||||
"ProcessingSettings",
|
||||
"ProcessingState",
|
||||
"TargetMetricsResponse",
|
||||
"TargetProcessingState",
|
||||
"EngineInfo",
|
||||
"EngineListResponse",
|
||||
"TemplateAssignment",
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
"""Device-related schemas (CRUD, settings, calibration, processing state, metrics)."""
|
||||
"""Device-related schemas (CRUD, calibration, device state)."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from wled_controller.core.processor_manager import DEFAULT_STATE_CHECK_INTERVAL
|
||||
|
||||
|
||||
class DeviceCreate(BaseModel):
|
||||
"""Request to create/attach a WLED device."""
|
||||
@@ -21,34 +19,6 @@ class DeviceUpdate(BaseModel):
|
||||
name: Optional[str] = Field(None, description="Device name", min_length=1, max_length=100)
|
||||
url: Optional[str] = Field(None, description="WLED device URL")
|
||||
enabled: Optional[bool] = Field(None, description="Whether device is enabled")
|
||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
|
||||
|
||||
|
||||
class ColorCorrection(BaseModel):
|
||||
"""Color correction settings."""
|
||||
|
||||
gamma: float = Field(default=2.2, description="Gamma correction", ge=0.1, le=5.0)
|
||||
saturation: float = Field(default=1.0, description="Saturation multiplier", ge=0.0, le=2.0)
|
||||
brightness: float = Field(default=1.0, description="Brightness multiplier", ge=0.0, le=1.0)
|
||||
|
||||
|
||||
class ProcessingSettings(BaseModel):
|
||||
"""Processing settings for a device."""
|
||||
|
||||
display_index: int = Field(default=0, description="Display to capture", ge=0)
|
||||
fps: int = Field(default=30, description="Target frames per second", ge=10, le=90)
|
||||
border_width: int = Field(default=10, description="Border width in pixels", ge=1, le=100)
|
||||
interpolation_mode: str = Field(default="average", description="LED color interpolation mode (average, median, dominant)")
|
||||
brightness: float = Field(default=1.0, description="Global brightness (0.0-1.0)", ge=0.0, le=1.0)
|
||||
smoothing: float = Field(default=0.3, description="Temporal smoothing factor (0.0=none, 1.0=full)", ge=0.0, le=1.0)
|
||||
state_check_interval: int = Field(
|
||||
default=DEFAULT_STATE_CHECK_INTERVAL, ge=5, le=600,
|
||||
description="Seconds between WLED health checks"
|
||||
)
|
||||
color_correction: Optional[ColorCorrection] = Field(
|
||||
default_factory=ColorCorrection,
|
||||
description="Color correction settings"
|
||||
)
|
||||
|
||||
|
||||
class Calibration(BaseModel):
|
||||
@@ -109,12 +79,7 @@ class DeviceResponse(BaseModel):
|
||||
url: str = Field(description="WLED device URL")
|
||||
led_count: int = Field(description="Total number of LEDs")
|
||||
enabled: bool = Field(description="Whether device is enabled")
|
||||
status: Literal["connected", "disconnected", "error"] = Field(
|
||||
description="Connection status"
|
||||
)
|
||||
settings: ProcessingSettings = Field(description="Processing settings")
|
||||
calibration: Optional[Calibration] = Field(None, description="Calibration configuration")
|
||||
picture_source_id: str = Field(default="", description="ID of assigned picture source")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
@@ -126,16 +91,10 @@ class DeviceListResponse(BaseModel):
|
||||
count: int = Field(description="Number of devices")
|
||||
|
||||
|
||||
class ProcessingState(BaseModel):
|
||||
"""Processing state for a device."""
|
||||
class DeviceStateResponse(BaseModel):
|
||||
"""Device health/connection state response."""
|
||||
|
||||
device_id: str = Field(description="Device ID")
|
||||
processing: bool = Field(description="Whether processing is active")
|
||||
fps_actual: Optional[float] = Field(None, description="Actual FPS achieved")
|
||||
fps_target: int = Field(description="Target FPS")
|
||||
display_index: int = Field(description="Current display index")
|
||||
last_update: Optional[datetime] = Field(None, description="Last successful update")
|
||||
errors: List[str] = Field(default_factory=list, description="Recent errors")
|
||||
wled_online: bool = Field(default=False, description="Whether WLED device is reachable")
|
||||
wled_latency_ms: Optional[float] = Field(None, description="WLED health check latency in ms")
|
||||
wled_name: Optional[str] = Field(None, description="WLED device name")
|
||||
@@ -145,17 +104,5 @@ class ProcessingState(BaseModel):
|
||||
wled_led_type: Optional[str] = Field(None, description="LED chip type (e.g. WS2812B, SK6812 RGBW)")
|
||||
wled_last_checked: Optional[datetime] = Field(None, description="Last health check time")
|
||||
wled_error: Optional[str] = Field(None, description="Last health check error")
|
||||
|
||||
|
||||
class MetricsResponse(BaseModel):
|
||||
"""Device metrics response."""
|
||||
|
||||
device_id: str = Field(description="Device ID")
|
||||
processing: bool = Field(description="Whether processing is active")
|
||||
fps_actual: Optional[float] = Field(None, description="Actual FPS")
|
||||
fps_target: int = Field(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")
|
||||
test_mode: bool = Field(default=False, description="Whether calibration test mode is active")
|
||||
test_mode_edges: List[str] = Field(default_factory=list, description="Currently lit edges in test mode")
|
||||
|
||||
114
server/src/wled_controller/api/schemas/picture_targets.py
Normal file
114
server/src/wled_controller/api/schemas/picture_targets.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Picture target schemas (CRUD, processing state, settings, metrics)."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from wled_controller.core.processor_manager import DEFAULT_STATE_CHECK_INTERVAL
|
||||
|
||||
|
||||
class ColorCorrection(BaseModel):
|
||||
"""Color correction settings."""
|
||||
|
||||
gamma: float = Field(default=2.2, description="Gamma correction", ge=0.1, le=5.0)
|
||||
saturation: float = Field(default=1.0, description="Saturation multiplier", ge=0.0, le=2.0)
|
||||
brightness: float = Field(default=1.0, description="Brightness multiplier", ge=0.0, le=1.0)
|
||||
|
||||
|
||||
class ProcessingSettings(BaseModel):
|
||||
"""Processing settings for a picture target."""
|
||||
|
||||
display_index: int = Field(default=0, description="Display to capture", ge=0)
|
||||
fps: int = Field(default=30, description="Target frames per second", ge=10, le=90)
|
||||
border_width: int = Field(default=10, description="Border width in pixels", ge=1, le=100)
|
||||
interpolation_mode: str = Field(default="average", description="LED color interpolation mode (average, median, dominant)")
|
||||
brightness: float = Field(default=1.0, description="Global brightness (0.0-1.0)", ge=0.0, le=1.0)
|
||||
smoothing: float = Field(default=0.3, description="Temporal smoothing factor (0.0=none, 1.0=full)", ge=0.0, le=1.0)
|
||||
state_check_interval: int = Field(
|
||||
default=DEFAULT_STATE_CHECK_INTERVAL, ge=5, le=600,
|
||||
description="Seconds between WLED health checks"
|
||||
)
|
||||
color_correction: Optional[ColorCorrection] = Field(
|
||||
default_factory=ColorCorrection,
|
||||
description="Color correction settings"
|
||||
)
|
||||
|
||||
|
||||
class PictureTargetCreate(BaseModel):
|
||||
"""Request to create a picture target."""
|
||||
|
||||
name: str = Field(description="Target name", min_length=1, max_length=100)
|
||||
target_type: str = Field(default="wled", description="Target type (wled)")
|
||||
device_id: str = Field(default="", description="WLED device ID")
|
||||
picture_source_id: str = Field(default="", description="Picture source ID")
|
||||
settings: Optional[ProcessingSettings] = Field(None, description="Processing settings")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
|
||||
|
||||
class PictureTargetUpdate(BaseModel):
|
||||
"""Request to update a picture target."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Target name", min_length=1, max_length=100)
|
||||
device_id: Optional[str] = Field(None, description="WLED device ID")
|
||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
|
||||
settings: Optional[ProcessingSettings] = Field(None, description="Processing settings")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
|
||||
|
||||
class PictureTargetResponse(BaseModel):
|
||||
"""Picture target response."""
|
||||
|
||||
id: str = Field(description="Target ID")
|
||||
name: str = Field(description="Target name")
|
||||
target_type: str = Field(description="Target type")
|
||||
device_id: str = Field(default="", description="WLED device ID")
|
||||
picture_source_id: str = Field(default="", description="Picture source ID")
|
||||
settings: ProcessingSettings = Field(description="Processing settings")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
|
||||
class PictureTargetListResponse(BaseModel):
|
||||
"""List of picture targets response."""
|
||||
|
||||
targets: List[PictureTargetResponse] = Field(description="List of picture targets")
|
||||
count: int = Field(description="Number of targets")
|
||||
|
||||
|
||||
class TargetProcessingState(BaseModel):
|
||||
"""Processing state for a picture target."""
|
||||
|
||||
target_id: str = Field(description="Target ID")
|
||||
device_id: str = Field(description="Device ID")
|
||||
processing: bool = Field(description="Whether processing is active")
|
||||
fps_actual: Optional[float] = Field(None, description="Actual FPS achieved")
|
||||
fps_target: int = Field(description="Target FPS")
|
||||
display_index: int = Field(description="Current display index")
|
||||
last_update: Optional[datetime] = Field(None, description="Last successful update")
|
||||
errors: List[str] = Field(default_factory=list, description="Recent errors")
|
||||
wled_online: bool = Field(default=False, description="Whether WLED device is reachable")
|
||||
wled_latency_ms: Optional[float] = Field(None, description="WLED health check latency in ms")
|
||||
wled_name: Optional[str] = Field(None, description="WLED device name")
|
||||
wled_version: Optional[str] = Field(None, description="WLED firmware version")
|
||||
wled_led_count: Optional[int] = Field(None, description="LED count reported by WLED device")
|
||||
wled_rgbw: Optional[bool] = Field(None, description="Whether WLED device uses RGBW LEDs")
|
||||
wled_led_type: Optional[str] = Field(None, description="LED chip type (e.g. WS2812B, SK6812 RGBW)")
|
||||
wled_last_checked: Optional[datetime] = Field(None, description="Last health check time")
|
||||
wled_error: Optional[str] = Field(None, description="Last health check error")
|
||||
|
||||
|
||||
class TargetMetricsResponse(BaseModel):
|
||||
"""Target metrics response."""
|
||||
|
||||
target_id: str = Field(description="Target ID")
|
||||
device_id: str = Field(description="Device ID")
|
||||
processing: bool = Field(description="Whether processing is active")
|
||||
fps_actual: Optional[float] = Field(None, description="Actual FPS")
|
||||
fps_target: int = Field(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")
|
||||
Reference in New Issue
Block a user