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:
@@ -29,7 +29,6 @@ from wled_controller.api.schemas.picture_targets import (
|
||||
PictureTargetListResponse,
|
||||
PictureTargetResponse,
|
||||
PictureTargetUpdate,
|
||||
ProcessingSettings as ProcessingSettingsSchema,
|
||||
TargetMetricsResponse,
|
||||
TargetProcessingState,
|
||||
)
|
||||
@@ -37,7 +36,6 @@ from wled_controller.config import get_config
|
||||
from wled_controller.core.capture_engines import EngineRegistry
|
||||
from wled_controller.core.filters import FilterRegistry, ImagePool
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.core.processing.processing_settings import ProcessingSettings
|
||||
from wled_controller.core.capture.screen_capture import (
|
||||
calculate_average_color,
|
||||
calculate_dominant_color,
|
||||
@@ -61,32 +59,6 @@ logger = get_logger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _settings_to_core(schema: ProcessingSettingsSchema) -> ProcessingSettings:
|
||||
"""Convert schema ProcessingSettings to core ProcessingSettings."""
|
||||
return ProcessingSettings(
|
||||
display_index=schema.display_index,
|
||||
fps=schema.fps,
|
||||
interpolation_mode=schema.interpolation_mode,
|
||||
brightness=schema.brightness,
|
||||
smoothing=schema.smoothing,
|
||||
standby_interval=schema.standby_interval,
|
||||
state_check_interval=schema.state_check_interval,
|
||||
)
|
||||
|
||||
|
||||
def _settings_to_schema(settings: ProcessingSettings) -> ProcessingSettingsSchema:
|
||||
"""Convert core ProcessingSettings to schema ProcessingSettings."""
|
||||
return ProcessingSettingsSchema(
|
||||
display_index=settings.display_index,
|
||||
fps=settings.fps,
|
||||
interpolation_mode=settings.interpolation_mode,
|
||||
brightness=settings.brightness,
|
||||
smoothing=settings.smoothing,
|
||||
standby_interval=settings.standby_interval,
|
||||
state_check_interval=settings.state_check_interval,
|
||||
)
|
||||
|
||||
|
||||
def _kc_settings_to_schema(settings: KeyColorsSettings) -> KeyColorsSettingsSchema:
|
||||
"""Convert core KeyColorsSettings to schema."""
|
||||
return KeyColorsSettingsSchema(
|
||||
@@ -117,8 +89,9 @@ def _target_to_response(target) -> PictureTargetResponse:
|
||||
name=target.name,
|
||||
target_type=target.target_type,
|
||||
device_id=target.device_id,
|
||||
picture_source_id=target.picture_source_id,
|
||||
settings=_settings_to_schema(target.settings),
|
||||
color_strip_source_id=target.color_strip_source_id,
|
||||
standby_interval=target.standby_interval,
|
||||
state_check_interval=target.state_check_interval,
|
||||
description=target.description,
|
||||
created_at=target.created_at,
|
||||
updated_at=target.updated_at,
|
||||
@@ -163,8 +136,6 @@ async def create_target(
|
||||
if not device:
|
||||
raise HTTPException(status_code=422, detail=f"Device {data.device_id} not found")
|
||||
|
||||
# Convert settings
|
||||
core_settings = _settings_to_core(data.settings) if data.settings else None
|
||||
kc_settings = _kc_schema_to_settings(data.key_colors_settings) if data.key_colors_settings else None
|
||||
|
||||
# Create in store
|
||||
@@ -172,8 +143,10 @@ async def create_target(
|
||||
name=data.name,
|
||||
target_type=data.target_type,
|
||||
device_id=data.device_id,
|
||||
color_strip_source_id=data.color_strip_source_id,
|
||||
standby_interval=data.standby_interval,
|
||||
state_check_interval=data.state_check_interval,
|
||||
picture_source_id=data.picture_source_id,
|
||||
settings=core_settings,
|
||||
key_colors_settings=kc_settings,
|
||||
description=data.description,
|
||||
)
|
||||
@@ -237,8 +210,6 @@ async def update_target(
|
||||
if not device:
|
||||
raise HTTPException(status_code=422, detail=f"Device {data.device_id} not found")
|
||||
|
||||
# Convert settings
|
||||
core_settings = _settings_to_core(data.settings) if data.settings else None
|
||||
kc_settings = _kc_schema_to_settings(data.key_colors_settings) if data.key_colors_settings else None
|
||||
|
||||
# Update in store
|
||||
@@ -246,8 +217,10 @@ async def update_target(
|
||||
target_id=target_id,
|
||||
name=data.name,
|
||||
device_id=data.device_id,
|
||||
color_strip_source_id=data.color_strip_source_id,
|
||||
standby_interval=data.standby_interval,
|
||||
state_check_interval=data.state_check_interval,
|
||||
picture_source_id=data.picture_source_id,
|
||||
settings=core_settings,
|
||||
key_colors_settings=kc_settings,
|
||||
description=data.description,
|
||||
)
|
||||
@@ -256,8 +229,10 @@ async def update_target(
|
||||
try:
|
||||
target.sync_with_manager(
|
||||
manager,
|
||||
settings_changed=data.settings is not None or data.key_colors_settings is not None,
|
||||
source_changed=data.picture_source_id is not None,
|
||||
settings_changed=(data.standby_interval is not None or
|
||||
data.state_check_interval is not None or
|
||||
data.key_colors_settings is not None),
|
||||
source_changed=data.color_strip_source_id is not None,
|
||||
device_changed=data.device_id is not None,
|
||||
)
|
||||
except ValueError:
|
||||
@@ -375,76 +350,6 @@ async def get_target_state(
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/api/v1/picture-targets/{target_id}/settings", tags=["Settings"])
|
||||
async def get_target_settings(
|
||||
target_id: str,
|
||||
_auth: AuthRequired,
|
||||
target_store: PictureTargetStore = Depends(get_picture_target_store),
|
||||
):
|
||||
"""Get processing settings for a target."""
|
||||
try:
|
||||
target = target_store.get_target(target_id)
|
||||
if isinstance(target, KeyColorsPictureTarget):
|
||||
return _kc_settings_to_schema(target.settings)
|
||||
if isinstance(target, WledPictureTarget):
|
||||
return _settings_to_schema(target.settings)
|
||||
return ProcessingSettingsSchema()
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/api/v1/picture-targets/{target_id}/settings", response_model=ProcessingSettingsSchema, tags=["Settings"])
|
||||
async def update_target_settings(
|
||||
target_id: str,
|
||||
settings: ProcessingSettingsSchema,
|
||||
_auth: AuthRequired,
|
||||
target_store: PictureTargetStore = Depends(get_picture_target_store),
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Update processing settings for a target.
|
||||
|
||||
Merges with existing settings so callers can send partial updates.
|
||||
"""
|
||||
try:
|
||||
target = target_store.get_target(target_id)
|
||||
if not isinstance(target, WledPictureTarget):
|
||||
raise HTTPException(status_code=400, detail="Target does not support processing settings")
|
||||
|
||||
existing = target.settings
|
||||
sent = settings.model_fields_set
|
||||
|
||||
# Merge: only override fields the client explicitly provided
|
||||
new_settings = ProcessingSettings(
|
||||
display_index=settings.display_index if 'display_index' in sent else existing.display_index,
|
||||
fps=settings.fps if 'fps' in sent else existing.fps,
|
||||
interpolation_mode=settings.interpolation_mode if 'interpolation_mode' in sent else existing.interpolation_mode,
|
||||
brightness=settings.brightness if 'brightness' in sent else existing.brightness,
|
||||
smoothing=settings.smoothing if 'smoothing' in sent else existing.smoothing,
|
||||
standby_interval=settings.standby_interval if 'standby_interval' in sent else existing.standby_interval,
|
||||
state_check_interval=settings.state_check_interval if 'state_check_interval' in sent else existing.state_check_interval,
|
||||
)
|
||||
|
||||
# Update in store
|
||||
target_store.update_target(target_id, settings=new_settings)
|
||||
|
||||
# Update in manager
|
||||
try:
|
||||
manager.update_target_settings(target_id, new_settings)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return _settings_to_schema(new_settings)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update target settings: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/api/v1/picture-targets/{target_id}/metrics", response_model=TargetMetricsResponse, tags=["Metrics"])
|
||||
async def get_target_metrics(
|
||||
target_id: str,
|
||||
|
||||
Reference in New Issue
Block a user