"""Pydantic schemas for API request and response models.""" from datetime import datetime from typing import Dict, List, Literal, Optional from pydantic import BaseModel, Field, HttpUrl from wled_controller.core.processor_manager import DEFAULT_STATE_CHECK_INTERVAL # Health and Version Schemas class HealthResponse(BaseModel): """Health check response.""" status: Literal["healthy", "unhealthy"] = Field(description="Service health status") timestamp: datetime = Field(description="Current server time") version: str = Field(description="Application version") class VersionResponse(BaseModel): """Version information response.""" version: str = Field(description="Application version") python_version: str = Field(description="Python version") api_version: str = Field(description="API version") # Display Schemas class DisplayInfo(BaseModel): """Display/monitor information.""" index: int = Field(description="Display index") name: str = Field(description="Display name") width: int = Field(description="Display width in pixels") height: int = Field(description="Display height in pixels") x: int = Field(description="Display X position") y: int = Field(description="Display Y position") is_primary: bool = Field(default=False, description="Whether this is the primary display") refresh_rate: int = Field(description="Display refresh rate in Hz") class DisplayListResponse(BaseModel): """List of available displays.""" displays: List[DisplayInfo] = Field(description="Available displays") count: int = Field(description="Number of displays") # Device Schemas class DeviceCreate(BaseModel): """Request to create/attach a WLED device.""" name: str = Field(description="Device name", min_length=1, max_length=100) url: str = Field(description="WLED device URL (e.g., http://192.168.1.100)") class DeviceUpdate(BaseModel): """Request to update device information.""" 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") 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=1, le=60) border_width: int = Field(default=10, description="Border width in pixels", ge=1, le=100) brightness: float = Field(default=1.0, description="Global brightness (0.0-1.0)", 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): """Calibration configuration for pixel-to-LED mapping.""" layout: Literal["clockwise", "counterclockwise"] = Field( default="clockwise", description="LED strip layout direction" ) start_position: Literal["top_left", "top_right", "bottom_left", "bottom_right"] = Field( default="bottom_left", description="Starting corner of the LED strip" ) offset: int = Field( default=0, ge=0, description="Number of LEDs from physical LED 0 to start corner (along strip direction)" ) leds_top: int = Field(default=0, ge=0, description="Number of LEDs on the top edge") leds_right: int = Field(default=0, ge=0, description="Number of LEDs on the right edge") leds_bottom: int = Field(default=0, ge=0, description="Number of LEDs on the bottom edge") leds_left: int = Field(default=0, ge=0, description="Number of LEDs on the left edge") # Per-edge span: fraction of screen side covered by LEDs (0.0–1.0) span_top_start: float = Field(default=0.0, ge=0.0, le=1.0, description="Start of top edge coverage") span_top_end: float = Field(default=1.0, ge=0.0, le=1.0, description="End of top edge coverage") span_right_start: float = Field(default=0.0, ge=0.0, le=1.0, description="Start of right edge coverage") span_right_end: float = Field(default=1.0, ge=0.0, le=1.0, description="End of right edge coverage") span_bottom_start: float = Field(default=0.0, ge=0.0, le=1.0, description="Start of bottom edge coverage") span_bottom_end: float = Field(default=1.0, ge=0.0, le=1.0, description="End of bottom edge coverage") span_left_start: float = Field(default=0.0, ge=0.0, le=1.0, description="Start of left edge coverage") span_left_end: float = Field(default=1.0, ge=0.0, le=1.0, description="End of left edge coverage") class CalibrationTestModeRequest(BaseModel): """Request to set calibration test mode with multiple edges.""" edges: Dict[str, List[int]] = Field( default_factory=dict, description="Map of active edge names to RGB colors. " "E.g. {'top': [255, 0, 0], 'left': [255, 255, 0]}. " "Empty dict = exit test mode." ) class CalibrationTestModeResponse(BaseModel): """Response for calibration test mode.""" test_mode: bool = Field(description="Whether test mode is active") active_edges: List[str] = Field(default_factory=list, description="Currently lit edges") device_id: str = Field(description="Device ID") class DeviceResponse(BaseModel): """Device information response.""" id: str = Field(description="Device ID") name: str = Field(description="Device name") 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") created_at: datetime = Field(description="Creation timestamp") updated_at: datetime = Field(description="Last update timestamp") class DeviceListResponse(BaseModel): """List of devices response.""" devices: List[DeviceResponse] = Field(description="List of devices") count: int = Field(description="Number of devices") # Processing State Schemas class ProcessingState(BaseModel): """Processing state for a device.""" 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 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") # Error Schemas class ErrorResponse(BaseModel): """Error response.""" error: str = Field(description="Error type") message: str = Field(description="Error message") detail: Optional[Dict] = Field(None, description="Additional error details") timestamp: datetime = Field(default_factory=datetime.utcnow, description="Error timestamp")