Simplify scenes to capture only target state, add target selector

- Remove DeviceBrightnessSnapshot and AutomationSnapshot from scene data model
- Simplify capture_current_snapshot and apply_scene_state to targets only
- Remove device/automation dependencies from scene preset API routes
- Add target selector (combobox + add/remove) to scene capture modal
- Fix stale profiles reference bug in scene_preset_store recapture
- Update automation engine call sites for simplified scene functions
- Sync scene presets cache between automations and scene-presets modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 18:55:11 +03:00
parent 0eb0f44ddb
commit ff4e7f8adb
14 changed files with 157 additions and 204 deletions

View File

@@ -310,22 +310,17 @@ class AutomationEngine:
# For "revert" mode, capture current state before activating
if automation.deactivation_mode == "revert":
from wled_controller.core.scenes.scene_activator import capture_current_snapshot
targets, devices, automations = capture_current_snapshot(
self._target_store, self._device_store, self._store, self._manager,
)
targets = capture_current_snapshot(self._target_store, self._manager)
self._pre_activation_snapshots[automation.id] = ScenePreset(
id=f"_revert_{automation.id}",
name=f"Pre-activation snapshot for {automation.name}",
targets=targets,
devices=devices,
profiles=automations,
)
# Apply the scene
from wled_controller.core.scenes.scene_activator import apply_scene_state
status, errors = await apply_scene_state(
preset, self._target_store, self._device_store, self._store,
self, self._manager, skip_automations=True,
preset, self._target_store, self._manager,
)
self._active_automations[automation.id] = True
@@ -352,11 +347,10 @@ class AutomationEngine:
if deactivation_mode == "revert":
snapshot = self._pre_activation_snapshots.pop(automation_id, None)
if snapshot and self._target_store and self._device_store:
if snapshot and self._target_store:
from wled_controller.core.scenes.scene_activator import apply_scene_state
status, errors = await apply_scene_state(
snapshot, self._target_store, self._device_store, self._store,
self, self._manager, skip_automations=True,
snapshot, self._target_store, self._manager,
)
if errors:
logger.warning(f"Automation {automation_id} revert errors: {errors}")
@@ -367,13 +361,12 @@ class AutomationEngine:
elif deactivation_mode == "fallback_scene":
fallback_id = automation.deactivation_scene_preset_id if automation else None
if fallback_id and self._scene_preset_store and self._target_store and self._device_store:
if fallback_id and self._scene_preset_store and self._target_store:
try:
fallback = self._scene_preset_store.get_preset(fallback_id)
from wled_controller.core.scenes.scene_activator import apply_scene_state
status, errors = await apply_scene_state(
fallback, self._target_store, self._device_store, self._store,
self, self._manager, skip_automations=True,
fallback, self._target_store, self._manager,
)
if errors:
logger.warning(f"Automation {automation_id} fallback errors: {errors}")

View File

@@ -3,15 +3,11 @@
These functions are used by both the scene-presets API route and the automation engine.
"""
from typing import List, Optional, Tuple
from typing import List, Optional, Set, Tuple
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.automation_store import AutomationStore
from wled_controller.storage.scene_preset import (
AutomationSnapshot,
DeviceBrightnessSnapshot,
ScenePreset,
TargetSnapshot,
)
@@ -22,16 +18,18 @@ logger = get_logger(__name__)
def capture_current_snapshot(
target_store: PictureTargetStore,
device_store: DeviceStore,
automation_store: AutomationStore,
processor_manager: ProcessorManager,
) -> Tuple[List[TargetSnapshot], List[DeviceBrightnessSnapshot], List[AutomationSnapshot]]:
"""Capture current system state as snapshot lists.
target_ids: Optional[Set[str]] = None,
) -> List[TargetSnapshot]:
"""Capture current target state as a snapshot list.
Returns (targets, devices, automations) snapshot tuples.
Args:
target_ids: If provided, only capture these targets. Captures all if None.
"""
targets = []
for t in target_store.get_all_targets():
if target_ids is not None and t.id not in target_ids:
continue
proc = processor_manager._processors.get(t.id)
running = proc.is_running if proc else False
targets.append(TargetSnapshot(
@@ -43,44 +41,20 @@ def capture_current_snapshot(
auto_start=getattr(t, "auto_start", False),
))
devices = []
for d in device_store.get_all_devices():
devices.append(DeviceBrightnessSnapshot(
device_id=d.id,
software_brightness=getattr(d, "software_brightness", 255),
))
automations = []
for a in automation_store.get_all_automations():
automations.append(AutomationSnapshot(
automation_id=a.id,
enabled=a.enabled,
))
return targets, devices, automations
return targets
async def apply_scene_state(
preset: ScenePreset,
target_store: PictureTargetStore,
device_store: DeviceStore,
automation_store: AutomationStore,
automation_engine,
processor_manager: ProcessorManager,
*,
skip_automations: bool = False,
) -> Tuple[str, List[str]]:
"""Apply a scene preset's state to the system.
Args:
preset: The scene preset to activate.
target_store: Target store for reading/updating targets.
device_store: Device store for reading/updating devices.
automation_store: Automation store for reading/updating automations.
automation_engine: Automation engine for deactivation and re-evaluation.
processor_manager: Processor manager for starting/stopping targets.
skip_automations: If True, skip toggling automation enable states (used when
called from the automation engine itself to avoid recursion).
Returns:
(status, errors) where status is "activated" or "partial" and
@@ -88,21 +62,7 @@ async def apply_scene_state(
"""
errors: List[str] = []
# 1. Toggle automation enable states
if not skip_automations:
for auto_snap in preset.automations:
try:
a = automation_store.get_automation(auto_snap.automation_id)
if a.enabled != auto_snap.enabled:
if not auto_snap.enabled:
await automation_engine.deactivate_if_active(auto_snap.automation_id)
automation_store.update_automation(auto_snap.automation_id, enabled=auto_snap.enabled)
except ValueError:
errors.append(f"Automation {auto_snap.automation_id} not found (skipped)")
except Exception as e:
errors.append(f"Automation {auto_snap.automation_id}: {e}")
# 2. Stop targets that should be stopped
# 1. Stop targets that should be stopped
for ts in preset.targets:
if not ts.running:
try:
@@ -112,7 +72,7 @@ async def apply_scene_state(
except Exception as e:
errors.append(f"Stop target {ts.target_id}: {e}")
# 3. Update target configs (CSS, brightness source, FPS)
# 2. Update target configs (CSS, brightness source, FPS)
for ts in preset.targets:
try:
target = target_store.get_target(ts.target_id)
@@ -147,7 +107,7 @@ async def apply_scene_state(
except Exception as e:
errors.append(f"Target {ts.target_id} config: {e}")
# 4. Start targets that should be running
# 3. Start targets that should be running
for ts in preset.targets:
if ts.running:
try:
@@ -157,28 +117,6 @@ async def apply_scene_state(
except Exception as e:
errors.append(f"Start target {ts.target_id}: {e}")
# 5. Set device brightness
for ds in preset.devices:
try:
device = device_store.get_device(ds.device_id)
if device.software_brightness != ds.software_brightness:
device_store.update_device(ds.device_id, software_brightness=ds.software_brightness)
# Update live processor brightness
dev_state = processor_manager._devices.get(ds.device_id)
if dev_state:
dev_state.software_brightness = ds.software_brightness
except ValueError:
errors.append(f"Device {ds.device_id} not found (skipped)")
except Exception as e:
errors.append(f"Device {ds.device_id} brightness: {e}")
# Trigger automation re-evaluation after all changes
if not skip_automations:
try:
await automation_engine.trigger_evaluate()
except Exception as e:
errors.append(f"Automation re-evaluation: {e}")
status = "activated" if not errors else "partial"
if errors:
logger.warning(f"Scene activation errors: {errors}")