Refactor core/ into logical sub-packages and split filter files

Reorganize the flat core/ directory (17 files) into three sub-packages:
- core/devices/ — LED device communication (led_client, wled/adalight clients, providers, DDP)
- core/processing/ — target processing pipeline (processor_manager, target processors, live streams, settings)
- core/capture/ — screen capture & calibration (screen_capture, calibration, pixel_processor, overlay)

Also split the monolithic filters/builtin.py (460 lines, 8 filters) into
individual files: brightness, saturation, gamma, downscaler, pixelate,
auto_crop, flip, color_correction.

Includes the ProcessorManager refactor from target-centric architecture:
ProcessorManager slimmed from ~1600 to ~490 lines with unified
_processors dict replacing duplicate _targets/_kc_targets dicts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 12:03:29 +03:00
parent 77dd342c4c
commit fc779eef39
50 changed files with 2740 additions and 2267 deletions

View File

@@ -1,6 +1,6 @@
"""Dependency injection for API routes."""
from wled_controller.core.processor_manager import ProcessorManager
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage import DeviceStore
from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore

View File

@@ -4,7 +4,7 @@ import httpx
from fastapi import APIRouter, HTTPException, Depends
from wled_controller.api.auth import AuthRequired
from wled_controller.core.led_client import (
from wled_controller.core.devices.led_client import (
get_all_providers,
get_device_capabilities,
get_provider,
@@ -26,11 +26,11 @@ from wled_controller.api.schemas.devices import (
DiscoveredDeviceResponse,
DiscoverDevicesResponse,
)
from wled_controller.core.calibration import (
from wled_controller.core.capture.calibration import (
calibration_from_dict,
calibration_to_dict,
)
from wled_controller.core.processor_manager import ProcessorManager
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage import DeviceStore
from wled_controller.storage.picture_target_store import PictureTargetStore
from wled_controller.utils import get_logger
@@ -50,6 +50,7 @@ def _device_to_response(device) -> DeviceResponse:
led_count=device.led_count,
enabled=device.enabled,
baud_rate=device.baud_rate,
auto_shutdown=device.auto_shutdown,
capabilities=sorted(get_device_capabilities(device.device_type)),
calibration=CalibrationSchema(**calibration_to_dict(device.calibration)),
created_at=device.created_at,
@@ -110,6 +111,11 @@ async def create_device(
detail=f"Failed to connect to {device_type} device at {device_url}: {e}"
)
# Resolve auto_shutdown default: True for adalight, False otherwise
auto_shutdown = device_data.auto_shutdown
if auto_shutdown is None:
auto_shutdown = device_type == "adalight"
# Create device in storage
device = store.create_device(
name=device_data.name,
@@ -117,6 +123,7 @@ async def create_device(
led_count=led_count,
device_type=device_type,
baud_rate=device_data.baud_rate,
auto_shutdown=auto_shutdown,
)
# Register in processor manager for health monitoring
@@ -127,6 +134,7 @@ async def create_device(
calibration=device.calibration,
device_type=device.device_type,
baud_rate=device.baud_rate,
auto_shutdown=device.auto_shutdown,
)
return _device_to_response(device)
@@ -233,6 +241,7 @@ async def update_device(
enabled=update_data.enabled,
led_count=update_data.led_count,
baud_rate=update_data.baud_rate,
auto_shutdown=update_data.auto_shutdown,
)
# Sync connection info in processor manager
@@ -246,6 +255,10 @@ async def update_device(
except ValueError:
pass
# Sync auto_shutdown in runtime state
if update_data.auto_shutdown is not None and device_id in manager._devices:
manager._devices[device_id].auto_shutdown = update_data.auto_shutdown
return _device_to_response(device)
except ValueError as e:
@@ -409,7 +422,6 @@ async def set_device_power(
body: dict,
_auth: AuthRequired,
store: DeviceStore = Depends(get_device_store),
manager = Depends(get_processor_manager),
):
"""Turn device on or off."""
device = store.get_device(device_id)
@@ -423,13 +435,11 @@ async def set_device_power(
raise HTTPException(status_code=400, detail="'on' must be a boolean")
try:
if device.device_type == "adalight":
if not on:
await manager.send_black_frame(device_id)
# "on" is a no-op for Adalight — next processing frame lights them up
else:
provider = get_provider(device.device_type)
await provider.set_power(device.url, on)
provider = get_provider(device.device_type)
await provider.set_power(
device.url, on,
led_count=device.led_count, baud_rate=device.baud_rate,
)
return {"on": on}
except Exception as e:
logger.error(f"Failed to set power for {device_id}: {e}")

View File

@@ -35,7 +35,7 @@ from wled_controller.api.schemas.picture_sources import (
)
from wled_controller.core.capture_engines import EngineRegistry
from wled_controller.core.filters import FilterRegistry, ImagePool
from wled_controller.core.processor_manager import ProcessorManager
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage import DeviceStore
from wled_controller.storage.picture_target_store import PictureTargetStore
from wled_controller.storage.template_store import TemplateStore

View File

@@ -36,8 +36,9 @@ from wled_controller.api.schemas.picture_targets import (
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.processor_manager import ProcessorManager, ProcessingSettings
from wled_controller.core.screen_capture import (
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,
calculate_median_color,
@@ -276,25 +277,16 @@ async def update_target(
description=data.description,
)
# Sync processor manager
if isinstance(target, WledPictureTarget):
try:
if data.settings is not None:
manager.update_target_settings(target_id, target.settings)
if data.picture_source_id is not None:
manager.update_target_source(target_id, target.picture_source_id)
if data.device_id is not None:
manager.update_target_device(target_id, target.device_id)
except ValueError:
pass
elif isinstance(target, KeyColorsPictureTarget):
try:
if data.key_colors_settings is not None:
manager.update_kc_target_settings(target_id, target.settings)
if data.picture_source_id is not None:
manager.update_kc_target_source(target_id, target.picture_source_id)
except ValueError:
pass
# Sync processor manager (unified API handles both target types)
try:
if data.settings is not None or data.key_colors_settings is not None:
manager.update_target_settings(target_id, target.settings)
if data.picture_source_id is not None:
manager.update_target_source(target_id, target.picture_source_id)
if data.device_id is not None and isinstance(target, WledPictureTarget):
manager.update_target_device(target_id, target.device_id)
except ValueError:
pass
return _target_to_response(target)
@@ -316,21 +308,15 @@ async def delete_target(
):
"""Delete a picture target. Stops processing first if active."""
try:
# Stop processing if running (WLED or KC)
# Stop processing if running
try:
if manager.is_kc_target(target_id):
await manager.stop_kc_processing(target_id)
elif manager.is_target_processing(target_id):
await manager.stop_processing(target_id)
await manager.stop_processing(target_id)
except ValueError:
pass
# Remove from manager (WLED or KC)
# Remove from manager
try:
if manager.is_kc_target(target_id):
manager.remove_kc_target(target_id)
else:
manager.remove_target(target_id)
manager.remove_target(target_id)
except (ValueError, RuntimeError):
pass
@@ -357,13 +343,10 @@ async def start_processing(
):
"""Start processing for a picture target."""
try:
# Verify target exists and dispatch by type
target = target_store.get_target(target_id)
# Verify target exists in store
target_store.get_target(target_id)
if isinstance(target, KeyColorsPictureTarget):
await manager.start_kc_processing(target_id)
else:
await manager.start_processing(target_id)
await manager.start_processing(target_id)
logger.info(f"Started processing for target {target_id}")
return {"status": "started", "target_id": target_id}
@@ -385,10 +368,7 @@ async def stop_processing(
):
"""Stop processing for a picture target."""
try:
if manager.is_kc_target(target_id):
await manager.stop_kc_processing(target_id)
else:
await manager.stop_processing(target_id)
await manager.stop_processing(target_id)
logger.info(f"Stopped processing for target {target_id}")
return {"status": "stopped", "target_id": target_id}
@@ -410,10 +390,7 @@ async def get_target_state(
):
"""Get current processing state for a target."""
try:
if manager.is_kc_target(target_id):
state = manager.get_kc_target_state(target_id)
else:
state = manager.get_target_state(target_id)
state = manager.get_target_state(target_id)
return TargetProcessingState(**state)
except ValueError as e:
@@ -513,10 +490,7 @@ async def get_target_metrics(
):
"""Get processing metrics for a target."""
try:
if manager.is_kc_target(target_id):
metrics = manager.get_kc_target_metrics(target_id)
else:
metrics = manager.get_target_metrics(target_id)
metrics = manager.get_target_metrics(target_id)
return TargetMetricsResponse(**metrics)
except ValueError as e:

View File

@@ -32,7 +32,7 @@ from wled_controller.api.schemas.postprocessing import (
)
from wled_controller.core.capture_engines import EngineRegistry
from wled_controller.core.filters import FilterRegistry, FilterInstance, ImagePool
from wled_controller.core.processor_manager import ProcessorManager
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage import DeviceStore
from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore

View File

@@ -13,7 +13,7 @@ from wled_controller.api.schemas.system import (
HealthResponse,
VersionResponse,
)
from wled_controller.core.screen_capture import get_available_displays
from wled_controller.core.capture.screen_capture import get_available_displays
from wled_controller.utils import get_logger
logger = get_logger(__name__)

View File

@@ -36,7 +36,7 @@ from wled_controller.api.schemas.filters import (
)
from wled_controller.core.capture_engines import EngineRegistry
from wled_controller.core.filters import FilterRegistry
from wled_controller.core.processor_manager import ProcessorManager
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage import DeviceStore
from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.picture_source_store import PictureSourceStore

View File

@@ -14,6 +14,7 @@ class DeviceCreate(BaseModel):
device_type: str = Field(default="wled", description="LED device type (e.g., wled, adalight)")
led_count: Optional[int] = Field(None, ge=1, le=10000, description="Number of LEDs (required for adalight)")
baud_rate: Optional[int] = Field(None, description="Serial baud rate (for adalight devices)")
auto_shutdown: Optional[bool] = Field(default=None, description="Turn off device when server stops (defaults to true for adalight)")
class DeviceUpdate(BaseModel):
@@ -24,6 +25,7 @@ class DeviceUpdate(BaseModel):
enabled: Optional[bool] = Field(None, description="Whether device is enabled")
led_count: Optional[int] = Field(None, ge=1, le=10000, description="Number of LEDs (for devices with manual_led_count capability)")
baud_rate: Optional[int] = Field(None, description="Serial baud rate (for adalight devices)")
auto_shutdown: Optional[bool] = Field(None, description="Turn off device when server stops")
class Calibration(BaseModel):
@@ -90,6 +92,7 @@ class DeviceResponse(BaseModel):
led_count: int = Field(description="Total number of LEDs")
enabled: bool = Field(description="Whether device is enabled")
baud_rate: Optional[int] = Field(None, description="Serial baud rate")
auto_shutdown: bool = Field(default=False, description="Turn off device when server stops")
capabilities: List[str] = Field(default_factory=list, description="Device type capabilities")
calibration: Optional[Calibration] = Field(None, description="Calibration configuration")
created_at: datetime = Field(description="Creation timestamp")

View File

@@ -5,7 +5,7 @@ from typing import Dict, List, Literal, Optional
from pydantic import BaseModel, Field
from wled_controller.core.processor_manager import DEFAULT_STATE_CHECK_INTERVAL
from wled_controller.core.processing.processing_settings import DEFAULT_STATE_CHECK_INTERVAL
class ColorCorrection(BaseModel):