Introduce Picture Targets to separate processing from devices
Add PictureTarget entity that bridges PictureSource to output device, separating processing settings from device connection/calibration state. This enables future target types (Art-Net, E1.31) and cleanly decouples "what to stream" from "where to stream." - Add PictureTarget/WledPictureTarget dataclasses and storage - Split ProcessorManager into DeviceState (health) + TargetState (processing) - Add /api/v1/picture-targets endpoints (CRUD, start/stop, settings, metrics) - Simplify device API (remove processing/settings/metrics endpoints) - Auto-migrate existing device settings to picture targets on first startup - Add Targets tab to WebUI with target cards and editor modal - Add en/ru locale keys for targets UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,11 +13,13 @@ from wled_controller import __version__
|
||||
from wled_controller.api import router
|
||||
from wled_controller.api.dependencies import init_dependencies
|
||||
from wled_controller.config import get_config
|
||||
from wled_controller.core.processor_manager import ProcessorManager
|
||||
from wled_controller.core.processor_manager import ProcessorManager, ProcessingSettings
|
||||
from wled_controller.storage import DeviceStore
|
||||
from wled_controller.storage.template_store import TemplateStore
|
||||
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
|
||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
||||
from wled_controller.storage.picture_target_store import PictureTargetStore
|
||||
from wled_controller.storage.picture_target import WledPictureTarget
|
||||
from wled_controller.utils import setup_logging, get_logger
|
||||
|
||||
# Initialize logging
|
||||
@@ -32,6 +34,7 @@ device_store = DeviceStore(config.storage.devices_file)
|
||||
template_store = TemplateStore(config.storage.templates_file)
|
||||
pp_template_store = PostprocessingTemplateStore(config.storage.postprocessing_templates_file)
|
||||
picture_source_store = PictureSourceStore(config.storage.picture_sources_file)
|
||||
picture_target_store = PictureTargetStore(config.storage.picture_targets_file)
|
||||
|
||||
processor_manager = ProcessorManager(
|
||||
picture_source_store=picture_source_store,
|
||||
@@ -40,6 +43,63 @@ processor_manager = ProcessorManager(
|
||||
)
|
||||
|
||||
|
||||
def _migrate_devices_to_targets():
|
||||
"""One-time migration: create picture targets from legacy device settings.
|
||||
|
||||
If the target store is empty and any device has legacy picture_source_id
|
||||
or settings in raw JSON, migrate them to WledPictureTargets.
|
||||
"""
|
||||
if picture_target_store.count() > 0:
|
||||
return # Already have targets, skip migration
|
||||
|
||||
raw = device_store.load_raw()
|
||||
devices_raw = raw.get("devices", {})
|
||||
if not devices_raw:
|
||||
return
|
||||
|
||||
migrated = 0
|
||||
for device_id, device_data in devices_raw.items():
|
||||
legacy_source_id = device_data.get("picture_source_id", "")
|
||||
legacy_settings = device_data.get("settings", {})
|
||||
|
||||
if not legacy_source_id and not legacy_settings:
|
||||
continue
|
||||
|
||||
# Build ProcessingSettings from legacy data
|
||||
from wled_controller.core.processor_manager import DEFAULT_STATE_CHECK_INTERVAL
|
||||
settings = ProcessingSettings(
|
||||
display_index=legacy_settings.get("display_index", 0),
|
||||
fps=legacy_settings.get("fps", 30),
|
||||
border_width=legacy_settings.get("border_width", 10),
|
||||
brightness=legacy_settings.get("brightness", 1.0),
|
||||
gamma=legacy_settings.get("gamma", 2.2),
|
||||
saturation=legacy_settings.get("saturation", 1.0),
|
||||
smoothing=legacy_settings.get("smoothing", 0.3),
|
||||
interpolation_mode=legacy_settings.get("interpolation_mode", "average"),
|
||||
state_check_interval=legacy_settings.get("state_check_interval", DEFAULT_STATE_CHECK_INTERVAL),
|
||||
)
|
||||
|
||||
device_name = device_data.get("name", device_id)
|
||||
target_name = f"{device_name} Target"
|
||||
|
||||
try:
|
||||
target = picture_target_store.create_target(
|
||||
name=target_name,
|
||||
target_type="wled",
|
||||
device_id=device_id,
|
||||
picture_source_id=legacy_source_id,
|
||||
settings=settings,
|
||||
description=f"Auto-migrated from device {device_name}",
|
||||
)
|
||||
migrated += 1
|
||||
logger.info(f"Migrated device {device_id} -> target {target.id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to migrate device {device_id} to target: {e}")
|
||||
|
||||
if migrated > 0:
|
||||
logger.info(f"Migration complete: created {migrated} picture target(s) from legacy device settings")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager.
|
||||
@@ -69,14 +129,18 @@ async def lifespan(app: FastAPI):
|
||||
logger.info(f"Authorized clients: {client_labels}")
|
||||
logger.info("All API requests require valid Bearer token authentication")
|
||||
|
||||
# Run migration from legacy device settings to picture targets
|
||||
_migrate_devices_to_targets()
|
||||
|
||||
# Initialize API dependencies
|
||||
init_dependencies(
|
||||
device_store, template_store, processor_manager,
|
||||
pp_template_store=pp_template_store,
|
||||
picture_source_store=picture_source_store,
|
||||
picture_target_store=picture_target_store,
|
||||
)
|
||||
|
||||
# Load existing devices into processor manager
|
||||
# Register devices in processor manager for health monitoring
|
||||
devices = device_store.get_all_devices()
|
||||
for device in devices:
|
||||
try:
|
||||
@@ -84,15 +148,32 @@ async def lifespan(app: FastAPI):
|
||||
device_id=device.id,
|
||||
device_url=device.url,
|
||||
led_count=device.led_count,
|
||||
settings=device.settings,
|
||||
calibration=device.calibration,
|
||||
picture_source_id=device.picture_source_id,
|
||||
)
|
||||
logger.info(f"Loaded device: {device.name} ({device.id})")
|
||||
logger.info(f"Registered device: {device.name} ({device.id})")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load device {device.id}: {e}")
|
||||
logger.error(f"Failed to register device {device.id}: {e}")
|
||||
|
||||
logger.info(f"Loaded {len(devices)} devices from storage")
|
||||
logger.info(f"Registered {len(devices)} devices for health monitoring")
|
||||
|
||||
# Register picture targets in processor manager
|
||||
targets = picture_target_store.get_all_targets()
|
||||
registered_targets = 0
|
||||
for target in targets:
|
||||
if isinstance(target, WledPictureTarget) and target.device_id:
|
||||
try:
|
||||
processor_manager.add_target(
|
||||
target_id=target.id,
|
||||
device_id=target.device_id,
|
||||
settings=target.settings,
|
||||
picture_source_id=target.picture_source_id,
|
||||
)
|
||||
registered_targets += 1
|
||||
logger.info(f"Registered target: {target.name} ({target.id})")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to register target {target.id}: {e}")
|
||||
|
||||
logger.info(f"Registered {registered_targets} picture target(s)")
|
||||
|
||||
# Start background health monitoring for all devices
|
||||
await processor_manager.start_health_monitoring()
|
||||
|
||||
Reference in New Issue
Block a user