Add LED skip start/end, rename standby_interval to keepalive_interval, remove migrations

LED skip: set first N and last M LEDs to black on a target. Color sources
(static, gradient, effect, color cycle) render across only the active
(non-skipped) LEDs. Processor pads with blacks before sending to device.

Rename standby_interval → keepalive_interval across all Python, API
schemas, and JS. from_dict falls back to old key for existing configs.

Remove legacy migration functions (_migrate_devices_to_targets,
_migrate_targets_to_color_strips) and legacy fields from target model.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 02:15:29 +03:00
parent f9a5fb68ed
commit e32bfab888
14 changed files with 168 additions and 163 deletions

View File

@@ -54,105 +54,6 @@ 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", "")
if not legacy_source_id:
continue
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,
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")
def _migrate_targets_to_color_strips():
"""One-time migration: create ColorStripSources from legacy WledPictureTarget data.
For each WledPictureTarget that has a legacy _legacy_picture_source_id (from old JSON)
but no color_strip_source_id, create a ColorStripSource and link it.
"""
from wled_controller.storage.wled_picture_target import WledPictureTarget
from wled_controller.core.capture.calibration import create_default_calibration
migrated = 0
for target in picture_target_store.get_all_targets():
if not isinstance(target, WledPictureTarget):
continue
if target.color_strip_source_id:
continue # already migrated
if not target._legacy_picture_source_id:
continue # no legacy source to migrate
legacy_settings = target._legacy_settings or {}
# Try to get calibration from device (old location)
device = device_store.get_device(target.device_id) if target.device_id else None
calibration = getattr(device, "_legacy_calibration", None) if device else None
if calibration is None:
calibration = create_default_calibration(0)
css_name = f"{target.name} Strip"
# Ensure unique name
existing_names = {s.name for s in color_strip_store.get_all_sources()}
if css_name in existing_names:
css_name = f"{target.name} Strip (migrated)"
try:
css = color_strip_store.create_source(
name=css_name,
source_type="picture",
picture_source_id=target._legacy_picture_source_id,
fps=legacy_settings.get("fps", 30),
brightness=legacy_settings.get("brightness", 1.0),
smoothing=legacy_settings.get("smoothing", 0.3),
interpolation_mode=legacy_settings.get("interpolation_mode", "average"),
calibration=calibration,
)
# Update target to reference the new CSS
target.color_strip_source_id = css.id
target.standby_interval = legacy_settings.get("standby_interval", 1.0)
target.state_check_interval = legacy_settings.get("state_check_interval", 30)
picture_target_store._save()
migrated += 1
logger.info(f"Migrated target {target.id} -> CSS {css.id} ({css_name})")
except Exception as e:
logger.error(f"Failed to migrate target {target.id} to CSS: {e}")
if migrated > 0:
logger.info(f"CSS migration complete: created {migrated} color strip source(s) from legacy targets")
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan manager.
@@ -182,10 +83,6 @@ async def lifespan(app: FastAPI):
logger.info(f"Authorized clients: {client_labels}")
logger.info("All API requests require valid Bearer token authentication")
# Run migrations
_migrate_devices_to_targets()
_migrate_targets_to_color_strips()
# Create profile engine (needs processor_manager)
profile_engine = ProfileEngine(profile_store, processor_manager)