"""LED picture target — sends color strip sources to an LED device.""" from dataclasses import dataclass, field from datetime import datetime from typing import List, Optional from wled_controller.storage.picture_target import PictureTarget DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds @dataclass class TargetSegment: """Maps a color strip source to a pixel range on the LED device. ``start`` is inclusive, ``end`` is exclusive. When a target has a single segment with ``end == 0`` the range auto-fits to the full device LED count. """ color_strip_source_id: str = "" start: int = 0 end: int = 0 reverse: bool = False def to_dict(self) -> dict: return { "color_strip_source_id": self.color_strip_source_id, "start": self.start, "end": self.end, "reverse": self.reverse, } @staticmethod def from_dict(d: dict) -> "TargetSegment": return TargetSegment( color_strip_source_id=d.get("color_strip_source_id", ""), start=d.get("start", 0), end=d.get("end", 0), reverse=d.get("reverse", False), ) @dataclass class WledPictureTarget(PictureTarget): """LED picture target — pairs an LED device with one or more ColorStripSources. Each segment maps a ColorStripSource to a pixel range on the device. Gaps between segments stay black. A single segment with ``end == 0`` auto-fits to the full device LED count. """ device_id: str = "" segments: List[TargetSegment] = field(default_factory=list) fps: int = 30 # target send FPS (1-90) keepalive_interval: float = 1.0 # seconds between keepalive sends when screen is static state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL def register_with_manager(self, manager) -> None: """Register this WLED target with the processor manager.""" if self.device_id: manager.add_target( target_id=self.id, device_id=self.device_id, segments=[s.to_dict() for s in self.segments], fps=self.fps, keepalive_interval=self.keepalive_interval, state_check_interval=self.state_check_interval, ) def sync_with_manager(self, manager, *, settings_changed: bool, segments_changed: bool = False, device_changed: bool = False) -> None: """Push changed fields to the processor manager.""" if settings_changed: manager.update_target_settings(self.id, { "fps": self.fps, "keepalive_interval": self.keepalive_interval, "state_check_interval": self.state_check_interval, }) if segments_changed: manager.update_target_segments(self.id, [s.to_dict() for s in self.segments]) if device_changed: manager.update_target_device(self.id, self.device_id) def update_fields(self, *, name=None, device_id=None, segments=None, fps=None, keepalive_interval=None, state_check_interval=None, description=None, **_kwargs) -> None: """Apply mutable field updates for WLED targets.""" super().update_fields(name=name, description=description) if device_id is not None: self.device_id = device_id if segments is not None: self.segments = [ TargetSegment.from_dict(s) if isinstance(s, dict) else s for s in segments ] if fps is not None: self.fps = fps if keepalive_interval is not None: self.keepalive_interval = keepalive_interval if state_check_interval is not None: self.state_check_interval = state_check_interval @property def has_picture_source(self) -> bool: return any(s.color_strip_source_id for s in self.segments) def to_dict(self) -> dict: """Convert to dictionary.""" d = super().to_dict() d["device_id"] = self.device_id d["segments"] = [s.to_dict() for s in self.segments] d["fps"] = self.fps d["keepalive_interval"] = self.keepalive_interval d["state_check_interval"] = self.state_check_interval return d @classmethod def from_dict(cls, data: dict) -> "WledPictureTarget": """Create from dictionary with backward compatibility.""" # Migrate old single-source format to segments if "segments" in data: segments = [TargetSegment.from_dict(s) for s in data["segments"]] elif "color_strip_source_id" in data: css_id = data.get("color_strip_source_id", "") skip_start = data.get("led_skip_start", 0) skip_end = data.get("led_skip_end", 0) if css_id: segments = [TargetSegment( color_strip_source_id=css_id, start=skip_start, end=0, # auto-fit; skip_end handled by processor )] else: segments = [] else: segments = [] return cls( id=data["id"], name=data["name"], target_type="led", device_id=data.get("device_id", ""), segments=segments, fps=data.get("fps", 30), keepalive_interval=data.get("keepalive_interval", data.get("standby_interval", 1.0)), state_check_interval=data.get("state_check_interval", DEFAULT_STATE_CHECK_INTERVAL), description=data.get("description"), created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())), updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())), )