Introduce ColorStripSource as first-class entity
Extracts color processing and calibration out of WledPictureTarget into a new PictureColorStripSource entity, enabling multiple LED targets to share one capture/processing pipeline. New entities & processing: - storage/color_strip_source.py: ColorStripSource + PictureColorStripSource models - storage/color_strip_store.py: JSON-backed CRUD store (prefix css_) - core/processing/color_strip_stream.py: ColorStripStream ABC + PictureColorStripStream (runs border-extract → map → smooth → brightness/sat/gamma in background thread) - core/processing/color_strip_stream_manager.py: ref-counted shared stream manager Modified storage/processing: - WledPictureTarget simplified to device_id + color_strip_source_id + standby_interval + state_check_interval - Device model: calibration field removed - WledTargetProcessor: acquires ColorStripStream from manager instead of running its own pipeline - ProcessorManager: wires ColorStripStreamManager into TargetContext API layer: - New routes: GET/POST/PUT/DELETE /api/v1/color-strip-sources, PUT calibration/test - Removed calibration endpoints from /devices - Updated /picture-targets CRUD for new target structure Frontend: - New color-strips.js module with CSS editor modal and card rendering - Calibration modal extended with CSS mode (css-id hidden field + device picker) - targets.js: Color Strip Sources section added to LED tab; target editor/card updated - app.js: imports and window globals for CSS + showCSSCalibration - en.json / ru.json: color_strip.* and targets.section.color_strips keys added Data migration runs at startup: existing WledPictureTargets are converted to reference a new PictureColorStripSource created from their old settings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,12 +6,6 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from wled_controller.core.capture.calibration import (
|
||||
CalibrationConfig,
|
||||
calibration_from_dict,
|
||||
calibration_to_dict,
|
||||
create_default_calibration,
|
||||
)
|
||||
from wled_controller.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -20,8 +14,9 @@ logger = get_logger(__name__)
|
||||
class Device:
|
||||
"""Represents a WLED device configuration.
|
||||
|
||||
A device is a holder of connection state and calibration options.
|
||||
Processing settings and picture source assignments live on PictureTargets.
|
||||
A device holds connection state and output settings.
|
||||
Calibration, processing settings, and picture source assignments
|
||||
now live on ColorStripSource and WledPictureTarget respectively.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -36,7 +31,6 @@ class Device:
|
||||
software_brightness: int = 255,
|
||||
auto_shutdown: bool = False,
|
||||
static_color: Optional[Tuple[int, int, int]] = None,
|
||||
calibration: Optional[CalibrationConfig] = None,
|
||||
created_at: Optional[datetime] = None,
|
||||
updated_at: Optional[datetime] = None,
|
||||
):
|
||||
@@ -50,9 +44,10 @@ class Device:
|
||||
self.software_brightness = software_brightness
|
||||
self.auto_shutdown = auto_shutdown
|
||||
self.static_color = static_color
|
||||
self.calibration = calibration or create_default_calibration(led_count)
|
||||
self.created_at = created_at or datetime.utcnow()
|
||||
self.updated_at = updated_at or datetime.utcnow()
|
||||
# Preserved from old JSON for migration — not written back
|
||||
self._legacy_calibration = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert device to dictionary."""
|
||||
@@ -63,7 +58,6 @@ class Device:
|
||||
"led_count": self.led_count,
|
||||
"enabled": self.enabled,
|
||||
"device_type": self.device_type,
|
||||
"calibration": calibration_to_dict(self.calibration),
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
@@ -81,20 +75,13 @@ class Device:
|
||||
def from_dict(cls, data: dict) -> "Device":
|
||||
"""Create device from dictionary.
|
||||
|
||||
Backward-compatible: ignores legacy 'settings' and 'picture_source_id'
|
||||
fields that have been migrated to PictureTarget.
|
||||
Backward-compatible: reads legacy 'calibration' field and stores it
|
||||
in _legacy_calibration for migration use only.
|
||||
"""
|
||||
calibration_data = data.get("calibration")
|
||||
calibration = (
|
||||
calibration_from_dict(calibration_data)
|
||||
if calibration_data
|
||||
else create_default_calibration(data["led_count"])
|
||||
)
|
||||
|
||||
static_color_raw = data.get("static_color")
|
||||
static_color = tuple(static_color_raw) if static_color_raw else None
|
||||
|
||||
return cls(
|
||||
device = cls(
|
||||
device_id=data["id"],
|
||||
name=data["name"],
|
||||
url=data["url"],
|
||||
@@ -105,11 +92,21 @@ class Device:
|
||||
software_brightness=data.get("software_brightness", 255),
|
||||
auto_shutdown=data.get("auto_shutdown", False),
|
||||
static_color=static_color,
|
||||
calibration=calibration,
|
||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
||||
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
|
||||
)
|
||||
|
||||
# Preserve old calibration for migration (never written back by to_dict)
|
||||
calibration_data = data.get("calibration")
|
||||
if calibration_data:
|
||||
try:
|
||||
from wled_controller.core.capture.calibration import calibration_from_dict
|
||||
device._legacy_calibration = calibration_from_dict(calibration_data)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return device
|
||||
|
||||
|
||||
class DeviceStore:
|
||||
"""Persistent storage for WLED devices."""
|
||||
@@ -129,7 +126,7 @@ class DeviceStore:
|
||||
def load(self):
|
||||
"""Load devices from storage file."""
|
||||
if not self.storage_file.exists():
|
||||
logger.info(f"Storage file does not exist, starting with empty store")
|
||||
logger.info("Storage file does not exist, starting with empty store")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -190,7 +187,6 @@ class DeviceStore:
|
||||
led_count: int,
|
||||
device_type: str = "wled",
|
||||
baud_rate: Optional[int] = None,
|
||||
calibration: Optional[CalibrationConfig] = None,
|
||||
auto_shutdown: bool = False,
|
||||
) -> Device:
|
||||
"""Create a new device."""
|
||||
@@ -203,7 +199,6 @@ class DeviceStore:
|
||||
led_count=led_count,
|
||||
device_type=device_type,
|
||||
baud_rate=baud_rate,
|
||||
calibration=calibration,
|
||||
auto_shutdown=auto_shutdown,
|
||||
)
|
||||
|
||||
@@ -229,7 +224,6 @@ class DeviceStore:
|
||||
led_count: Optional[int] = None,
|
||||
enabled: Optional[bool] = None,
|
||||
baud_rate: Optional[int] = None,
|
||||
calibration: Optional[CalibrationConfig] = None,
|
||||
auto_shutdown: Optional[bool] = None,
|
||||
) -> Device:
|
||||
"""Update device."""
|
||||
@@ -249,13 +243,6 @@ class DeviceStore:
|
||||
device.baud_rate = baud_rate
|
||||
if auto_shutdown is not None:
|
||||
device.auto_shutdown = auto_shutdown
|
||||
if calibration is not None:
|
||||
if calibration.get_total_leds() != device.led_count:
|
||||
raise ValueError(
|
||||
f"Calibration LED count ({calibration.get_total_leds()}) "
|
||||
f"does not match device LED count ({device.led_count})"
|
||||
)
|
||||
device.calibration = calibration
|
||||
|
||||
device.updated_at = datetime.utcnow()
|
||||
self.save()
|
||||
|
||||
Reference in New Issue
Block a user