Remove target segments, use single color strip source per target

Segments are redundant now that the "mapped" CSS type handles spatial
multiplexing internally. Each target now references one color_strip_source_id
instead of an array of segments with start/end/reverse ranges.

Backward compat: existing targets with old segments format are migrated
on load by extracting the first segment's CSS source ID.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 00:00:26 +03:00
parent 9efb08acb6
commit 808037775f
14 changed files with 171 additions and 513 deletions

View File

@@ -7,7 +7,7 @@ from pathlib import Path
from typing import Dict, List, Optional
from wled_controller.storage.picture_target import PictureTarget
from wled_controller.storage.wled_picture_target import TargetSegment, WledPictureTarget
from wled_controller.storage.wled_picture_target import WledPictureTarget
from wled_controller.storage.key_colors_picture_target import (
KeyColorsSettings,
KeyColorsPictureTarget,
@@ -101,7 +101,7 @@ class PictureTargetStore:
name: str,
target_type: str,
device_id: str = "",
segments: Optional[List[dict]] = None,
color_strip_source_id: str = "",
fps: int = 30,
keepalive_interval: float = 1.0,
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
@@ -126,14 +126,12 @@ class PictureTargetStore:
now = datetime.utcnow()
if target_type == "led":
seg_list = [TargetSegment.from_dict(s) for s in segments] if segments else []
target: PictureTarget = WledPictureTarget(
id=target_id,
name=name,
target_type="led",
device_id=device_id,
segments=seg_list,
color_strip_source_id=color_strip_source_id,
fps=fps,
keepalive_interval=keepalive_interval,
state_check_interval=state_check_interval,
@@ -166,7 +164,7 @@ class PictureTargetStore:
target_id: str,
name: Optional[str] = None,
device_id: Optional[str] = None,
segments: Optional[List[dict]] = None,
color_strip_source_id: Optional[str] = None,
fps: Optional[int] = None,
keepalive_interval: Optional[float] = None,
state_check_interval: Optional[int] = None,
@@ -192,7 +190,7 @@ class PictureTargetStore:
target.update_fields(
name=name,
device_id=device_id,
segments=segments,
color_strip_source_id=color_strip_source_id,
fps=fps,
keepalive_interval=keepalive_interval,
state_check_interval=state_check_interval,
@@ -239,7 +237,7 @@ class PictureTargetStore:
return [
target.name for target in self._targets.values()
if isinstance(target, WledPictureTarget)
and any(seg.color_strip_source_id == css_id for seg in target.segments)
and target.color_strip_source_id == css_id
]
def count(self) -> int:

View File

@@ -1,56 +1,20 @@
"""LED picture target — sends color strip sources to an LED device."""
from dataclasses import dataclass, field
from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional
from typing import 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.
"""
"""LED picture target — pairs an LED device with a ColorStripSource."""
device_id: str = ""
segments: List[TargetSegment] = field(default_factory=list)
color_strip_source_id: str = ""
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
@@ -61,14 +25,14 @@ class WledPictureTarget(PictureTarget):
manager.add_target(
target_id=self.id,
device_id=self.device_id,
segments=[s.to_dict() for s in self.segments],
color_strip_source_id=self.color_strip_source_id,
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,
css_changed: bool = False,
device_changed: bool = False) -> None:
"""Push changed fields to the processor manager."""
if settings_changed:
@@ -77,23 +41,20 @@ class WledPictureTarget(PictureTarget):
"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 css_changed:
manager.update_target_css(self.id, self.color_strip_source_id)
if device_changed:
manager.update_target_device(self.id, self.device_id)
def update_fields(self, *, name=None, device_id=None, segments=None,
def update_fields(self, *, name=None, device_id=None, color_strip_source_id=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 color_strip_source_id is not None:
self.color_strip_source_id = color_strip_source_id
if fps is not None:
self.fps = fps
if keepalive_interval is not None:
@@ -103,13 +64,13 @@ class WledPictureTarget(PictureTarget):
@property
def has_picture_source(self) -> bool:
return any(s.color_strip_source_id for s in self.segments)
return bool(self.color_strip_source_id)
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["color_strip_source_id"] = self.color_strip_source_id
d["fps"] = self.fps
d["keepalive_interval"] = self.keepalive_interval
d["state_check_interval"] = self.state_check_interval
@@ -118,30 +79,22 @@ class WledPictureTarget(PictureTarget):
@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 = []
# New format: direct color_strip_source_id
if "color_strip_source_id" in data:
css_id = data["color_strip_source_id"]
# Old format: segments array — take first segment's css_id
elif "segments" in data:
segs = data["segments"]
css_id = segs[0].get("color_strip_source_id", "") if segs else ""
else:
segments = []
css_id = ""
return cls(
id=data["id"],
name=data["name"],
target_type="led",
device_id=data.get("device_id", ""),
segments=segments,
color_strip_source_id=css_id,
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),