CSS: add StaticColorStripSource type with auto-sized LED count

Introduces a new 'static' source type that fills all device LEDs with a
single constant RGB color — no screen capture or processing required.

- StaticColorStripSource storage model (color + led_count=0 auto-size)
- StaticColorStripStream: no background thread, configure() sizes to device
  LED count at processor start; hot-updates preserve runtime size
- ColorStripStreamManager dispatches static sources (no LiveStream needed)
- WledTargetProcessor calls stream.configure(device_led_count) on start
- API schemas/routes: source_type Literal["picture","static"]; color field;
  overlay/calibration-test endpoints return 400 for static
- Frontend: type selector modal, color picker, type-aware card rendering
  (🎨 icon + color swatch), LED count field hidden for static type
- Locale keys: color_strip.type, color_strip.static_color (en + ru)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 17:49:48 +03:00
parent 0a23cb7043
commit 2a8e2daefc
12 changed files with 430 additions and 155 deletions

View File

@@ -6,9 +6,9 @@ calibration, color correction, smoothing, and FPS.
Current types:
PictureColorStripSource — derives LED colors from a PictureSource (screen capture)
StaticColorStripSource — constant solid color fills all LEDs
Future types (not yet implemented):
StaticColorStripSource — constant solid colors
GradientColorStripSource — animated gradient
"""
@@ -53,6 +53,7 @@ class ColorStripSource:
"interpolation_mode": None,
"calibration": None,
"led_count": None,
"color": None,
}
@staticmethod
@@ -85,7 +86,20 @@ class ColorStripSource:
else CalibrationConfig(layout="clockwise", start_position="bottom_left")
)
# Only "picture" type for now; extend with elif branches for future types
if source_type == "static":
raw_color = data.get("color")
color = (
raw_color if isinstance(raw_color, list) and len(raw_color) == 3
else [255, 255, 255]
)
return StaticColorStripSource(
id=sid, name=name, source_type="static",
created_at=created_at, updated_at=updated_at, description=description,
color=color,
led_count=data.get("led_count") or 0,
)
# Default: "picture" type
return PictureColorStripSource(
id=sid, name=name, source_type=source_type,
created_at=created_at, updated_at=updated_at, description=description,
@@ -133,3 +147,22 @@ class PictureColorStripSource(ColorStripSource):
d["calibration"] = calibration_to_dict(self.calibration)
d["led_count"] = self.led_count
return d
@dataclass
class StaticColorStripSource(ColorStripSource):
"""Color strip source that fills all LEDs with a single static color.
No capture or processing — the entire LED strip is set to one constant
RGB color. Useful for solid-color accents or as a placeholder while
a PictureColorStripSource is being configured.
"""
color: list = field(default_factory=lambda: [255, 255, 255]) # [R, G, B]
led_count: int = 0 # 0 = use device LED count
def to_dict(self) -> dict:
d = super().to_dict()
d["color"] = list(self.color)
d["led_count"] = self.led_count
return d

View File

@@ -10,6 +10,7 @@ from wled_controller.core.capture.calibration import CalibrationConfig, calibrat
from wled_controller.storage.color_strip_source import (
ColorStripSource,
PictureColorStripSource,
StaticColorStripSource,
)
from wled_controller.utils import get_logger
@@ -99,6 +100,7 @@ class ColorStripStore:
interpolation_mode: str = "average",
calibration=None,
led_count: int = 0,
color: Optional[list] = None,
description: Optional[str] = None,
) -> ColorStripSource:
"""Create a new color strip source.
@@ -113,29 +115,41 @@ class ColorStripStore:
if source.name == name:
raise ValueError(f"Color strip source with name '{name}' already exists")
if calibration is None:
calibration = CalibrationConfig(layout="clockwise", start_position="bottom_left")
source_id = f"css_{uuid.uuid4().hex[:8]}"
now = datetime.utcnow()
source = PictureColorStripSource(
id=source_id,
name=name,
source_type=source_type,
created_at=now,
updated_at=now,
description=description,
picture_source_id=picture_source_id,
fps=fps,
brightness=brightness,
saturation=saturation,
gamma=gamma,
smoothing=smoothing,
interpolation_mode=interpolation_mode,
calibration=calibration,
led_count=led_count,
)
if source_type == "static":
rgb = color if isinstance(color, list) and len(color) == 3 else [255, 255, 255]
source = StaticColorStripSource(
id=source_id,
name=name,
source_type="static",
created_at=now,
updated_at=now,
description=description,
color=rgb,
led_count=led_count,
)
else:
if calibration is None:
calibration = CalibrationConfig(layout="clockwise", start_position="bottom_left")
source = PictureColorStripSource(
id=source_id,
name=name,
source_type=source_type,
created_at=now,
updated_at=now,
description=description,
picture_source_id=picture_source_id,
fps=fps,
brightness=brightness,
saturation=saturation,
gamma=gamma,
smoothing=smoothing,
interpolation_mode=interpolation_mode,
calibration=calibration,
led_count=led_count,
)
self._sources[source_id] = source
self._save()
@@ -156,6 +170,7 @@ class ColorStripStore:
interpolation_mode: Optional[str] = None,
calibration=None,
led_count: Optional[int] = None,
color: Optional[list] = None,
description: Optional[str] = None,
) -> ColorStripSource:
"""Update an existing color strip source.
@@ -196,6 +211,12 @@ class ColorStripStore:
source.calibration = calibration
if led_count is not None:
source.led_count = led_count
elif isinstance(source, StaticColorStripSource):
if color is not None:
if isinstance(color, list) and len(color) == 3:
source.color = color
if led_count is not None:
source.led_count = led_count
source.updated_at = datetime.utcnow()
self._save()