"""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") tags: List[str] = Field(default_factory=list, description="User-defined tags") # DMX (Art-Net / sACN) fields dmx_protocol: Optional[str] = Field(None, description="DMX protocol: artnet or sacn") dmx_start_universe: Optional[int] = Field(None, ge=0, le=32767, description="DMX start universe") dmx_start_channel: Optional[int] = Field(None, ge=1, le=512, description="DMX start channel (1-512)") # ESP-NOW fields espnow_peer_mac: Optional[str] = Field(None, description="ESP-NOW peer MAC address (e.g. AA:BB:CC:DD:EE:FF)") espnow_channel: Optional[int] = Field(None, ge=1, le=14, description="ESP-NOW WiFi channel (1-14)") # Philips Hue fields hue_username: Optional[str] = Field(None, description="Hue bridge username (from pairing)") hue_client_key: Optional[str] = Field(None, description="Hue entertainment client key (hex)") hue_entertainment_group_id: Optional[str] = Field(None, description="Hue entertainment group/zone ID") # SPI Direct fields spi_speed_hz: Optional[int] = Field(None, ge=100000, le=4000000, description="SPI clock speed in Hz") spi_led_type: Optional[str] = Field(None, description="LED chipset: WS2812, WS2812B, WS2811, SK6812, SK6812_RGBW") # Razer Chroma fields chroma_device_type: Optional[str] = Field(None, description="Chroma peripheral type: keyboard, mouse, mousepad, headset, chromalink, keypad") # SteelSeries GameSense fields gamesense_device_type: Optional[str] = Field(None, description="GameSense device type: keyboard, mouse, headset, mousepad, indicator") default_css_processing_template_id: Optional[str] = Field(None, description="Default color strip processing template ID") 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") tags: Optional[List[str]] = None dmx_protocol: Optional[str] = Field(None, description="DMX protocol: artnet or sacn") dmx_start_universe: Optional[int] = Field(None, ge=0, le=32767, description="DMX start universe") dmx_start_channel: Optional[int] = Field(None, ge=1, le=512, description="DMX start channel (1-512)") espnow_peer_mac: Optional[str] = Field(None, description="ESP-NOW peer MAC address") espnow_channel: Optional[int] = Field(None, ge=1, le=14, description="ESP-NOW WiFi channel") hue_username: Optional[str] = Field(None, description="Hue bridge username") hue_client_key: Optional[str] = Field(None, description="Hue entertainment client key") hue_entertainment_group_id: Optional[str] = Field(None, description="Hue entertainment group ID") spi_speed_hz: Optional[int] = Field(None, ge=100000, le=4000000, description="SPI clock speed") spi_led_type: Optional[str] = Field(None, description="LED chipset type") chroma_device_type: Optional[str] = Field(None, description="Chroma peripheral type") gamesense_device_type: Optional[str] = Field(None, description="GameSense device type") default_css_processing_template_id: Optional[str] = Field(None, description="Default color strip processing template ID") 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") tags: List[str] = Field(default_factory=list, description="User-defined tags") dmx_protocol: str = Field(default="artnet", description="DMX protocol: artnet or sacn") dmx_start_universe: int = Field(default=0, description="DMX start universe") dmx_start_channel: int = Field(default=1, description="DMX start channel (1-512)") espnow_peer_mac: str = Field(default="", description="ESP-NOW peer MAC address") espnow_channel: int = Field(default=1, description="ESP-NOW WiFi channel") hue_username: str = Field(default="", description="Hue bridge username") hue_client_key: str = Field(default="", description="Hue entertainment client key") hue_entertainment_group_id: str = Field(default="", description="Hue entertainment group ID") spi_speed_hz: int = Field(default=800000, description="SPI clock speed in Hz") spi_led_type: str = Field(default="WS2812B", description="LED chipset type") chroma_device_type: str = Field(default="chromalink", description="Chroma peripheral type") gamesense_device_type: str = Field(default="keyboard", description="GameSense device type") default_css_processing_template_id: str = Field(default="", description="Default color strip processing template ID") 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")