"""Device-related schemas (CRUD, calibration, device state).""" from datetime import datetime from typing import Dict, List, Literal, Optional from pydantic import BaseModel, Field class DeviceCreate(BaseModel): """Request to create/attach an LED device.""" name: str = Field(description="Device name", min_length=1, max_length=100) url: str = Field(description="Device URL (e.g., http://192.168.1.100 or COM3)") device_type: str = Field(default="wled", description="LED device type (e.g., wled, adalight)") led_count: Optional[int] = Field(None, ge=1, le=10000, description="Number of LEDs (required for adalight)") baud_rate: Optional[int] = Field(None, description="Serial baud rate (for adalight devices)") auto_shutdown: Optional[bool] = Field(default=None, description="Turn off device when server stops (defaults to true for adalight)") send_latency_ms: Optional[int] = Field(None, ge=0, le=5000, description="Simulated send latency in ms (mock devices)") rgbw: Optional[bool] = Field(None, description="RGBW mode (mock devices)") zone_mode: Optional[str] = Field(None, description="OpenRGB zone mode: combined or separate") 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="Device URL or serial port") enabled: Optional[bool] = Field(None, description="Whether device is enabled") led_count: Optional[int] = Field(None, ge=1, le=10000, description="Number of LEDs (for devices with manual_led_count capability)") baud_rate: Optional[int] = Field(None, description="Serial baud rate (for adalight devices)") auto_shutdown: Optional[bool] = Field(None, description="Turn off device when server stops") send_latency_ms: Optional[int] = Field(None, ge=0, le=5000, description="Simulated send latency in ms (mock devices)") rgbw: Optional[bool] = Field(None, description="RGBW mode (mock devices)") zone_mode: Optional[str] = Field(None, description="OpenRGB zone mode: combined or separate") class CalibrationLineSchema(BaseModel): """One LED line in advanced calibration.""" picture_source_id: str = Field(description="Picture source (monitor) to sample from") edge: Literal["top", "right", "bottom", "left"] = Field(description="Screen edge to sample") led_count: int = Field(ge=1, description="Number of LEDs in this line") span_start: float = Field(default=0.0, ge=0.0, le=1.0, description="Start fraction along edge") span_end: float = Field(default=1.0, ge=0.0, le=1.0, description="End fraction along edge") reverse: bool = Field(default=False, description="Reverse LED direction") border_width: int = Field(default=10, ge=1, le=100, description="Sampling depth in pixels") class Calibration(BaseModel): """Calibration configuration for pixel-to-LED mapping.""" mode: Literal["simple", "advanced"] = Field( default="simple", description="Calibration mode: simple (4-edge) or advanced (multi-source lines)" ) # Advanced mode: ordered list of lines lines: Optional[List[CalibrationLineSchema]] = Field( default=None, description="Line list for advanced mode (ignored in simple mode)" ) # Simple mode fields 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") # Skip LEDs at start/end of strip skip_leds_start: int = Field(default=0, ge=0, description="LEDs to skip (black out) at the start of the strip") skip_leds_end: int = Field(default=0, ge=0, description="LEDs to skip (black out) at the end of the strip") border_width: int = Field(default=10, ge=1, le=100, description="Border width in pixels for edge sampling") 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="Device URL") device_type: str = Field(default="wled", description="LED device type") led_count: int = Field(description="Total number of LEDs") enabled: bool = Field(description="Whether device is enabled") baud_rate: Optional[int] = Field(None, description="Serial baud rate") auto_shutdown: bool = Field(default=False, description="Restore device to idle state when targets stop") send_latency_ms: int = Field(default=0, description="Simulated send latency in ms (mock devices)") rgbw: bool = Field(default=False, description="RGBW mode (mock devices)") zone_mode: str = Field(default="combined", description="OpenRGB zone mode: combined or separate") capabilities: List[str] = Field(default_factory=list, description="Device type capabilities") 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") class DeviceStateResponse(BaseModel): """Device health/connection state response.""" device_id: str = Field(description="Device ID") device_type: str = Field(default="wled", description="LED device type") 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") 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") class DiscoveredDeviceResponse(BaseModel): """A single device found via network discovery.""" name: str = Field(description="Device name (from mDNS or firmware)") url: str = Field(description="Device URL") device_type: str = Field(default="wled", description="Device type") ip: str = Field(description="IP address") mac: str = Field(default="", description="MAC address") led_count: Optional[int] = Field(None, description="LED count (if reachable)") version: Optional[str] = Field(None, description="Firmware version") already_added: bool = Field(default=False, description="Whether this device is already in the system") class DiscoverDevicesResponse(BaseModel): """Response from device discovery scan.""" devices: List[DiscoveredDeviceResponse] = Field(description="Discovered devices") count: int = Field(description="Total devices found") scan_duration_ms: float = Field(description="How long the scan took in milliseconds") class OpenRGBZoneResponse(BaseModel): """A single zone on an OpenRGB device.""" name: str = Field(description="Zone name (e.g. JRAINBOW2)") led_count: int = Field(description="Number of LEDs in this zone") zone_type: str = Field(description="Zone type (linear, single, matrix)") class OpenRGBZonesResponse(BaseModel): """Response from OpenRGB zone listing.""" device_name: str = Field(description="OpenRGB device name") zones: List[OpenRGBZoneResponse] = Field(description="Available zones")