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:
@@ -7,9 +7,6 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
|
|
||||||
from wled_controller.api.auth import AuthRequired
|
from wled_controller.api.auth import AuthRequired
|
||||||
from wled_controller.api.dependencies import (
|
from wled_controller.api.dependencies import (
|
||||||
get_automation_engine,
|
|
||||||
get_automation_store,
|
|
||||||
get_device_store,
|
|
||||||
get_picture_target_store,
|
get_picture_target_store,
|
||||||
get_processor_manager,
|
get_processor_manager,
|
||||||
get_scene_preset_store,
|
get_scene_preset_store,
|
||||||
@@ -26,12 +23,9 @@ from wled_controller.core.scenes.scene_activator import (
|
|||||||
apply_scene_state,
|
apply_scene_state,
|
||||||
capture_current_snapshot,
|
capture_current_snapshot,
|
||||||
)
|
)
|
||||||
from wled_controller.storage import DeviceStore
|
|
||||||
from wled_controller.storage.picture_target_store import PictureTargetStore
|
from wled_controller.storage.picture_target_store import PictureTargetStore
|
||||||
from wled_controller.storage.automation_store import AutomationStore
|
|
||||||
from wled_controller.storage.scene_preset import ScenePreset
|
from wled_controller.storage.scene_preset import ScenePreset
|
||||||
from wled_controller.storage.scene_preset_store import ScenePresetStore
|
from wled_controller.storage.scene_preset_store import ScenePresetStore
|
||||||
from wled_controller.core.automations.automation_engine import AutomationEngine
|
|
||||||
from wled_controller.utils import get_logger
|
from wled_controller.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -52,14 +46,6 @@ def _preset_to_response(preset: ScenePreset) -> ScenePresetResponse:
|
|||||||
"fps": t.fps,
|
"fps": t.fps,
|
||||||
"auto_start": t.auto_start,
|
"auto_start": t.auto_start,
|
||||||
} for t in preset.targets],
|
} for t in preset.targets],
|
||||||
devices=[{
|
|
||||||
"device_id": d.device_id,
|
|
||||||
"software_brightness": d.software_brightness,
|
|
||||||
} for d in preset.devices],
|
|
||||||
automations=[{
|
|
||||||
"automation_id": a.automation_id,
|
|
||||||
"enabled": a.enabled,
|
|
||||||
} for a in preset.automations],
|
|
||||||
order=preset.order,
|
order=preset.order,
|
||||||
created_at=preset.created_at,
|
created_at=preset.created_at,
|
||||||
updated_at=preset.updated_at,
|
updated_at=preset.updated_at,
|
||||||
@@ -79,14 +65,11 @@ async def create_scene_preset(
|
|||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
store: ScenePresetStore = Depends(get_scene_preset_store),
|
store: ScenePresetStore = Depends(get_scene_preset_store),
|
||||||
target_store: PictureTargetStore = Depends(get_picture_target_store),
|
target_store: PictureTargetStore = Depends(get_picture_target_store),
|
||||||
device_store: DeviceStore = Depends(get_device_store),
|
|
||||||
automation_store: AutomationStore = Depends(get_automation_store),
|
|
||||||
manager: ProcessorManager = Depends(get_processor_manager),
|
manager: ProcessorManager = Depends(get_processor_manager),
|
||||||
):
|
):
|
||||||
"""Capture current state as a new scene preset."""
|
"""Capture current state as a new scene preset."""
|
||||||
targets, devices, automations = capture_current_snapshot(
|
target_ids = set(data.target_ids) if data.target_ids is not None else None
|
||||||
target_store, device_store, automation_store, manager,
|
targets = capture_current_snapshot(target_store, manager, target_ids)
|
||||||
)
|
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
preset = ScenePreset(
|
preset = ScenePreset(
|
||||||
@@ -95,8 +78,6 @@ async def create_scene_preset(
|
|||||||
description=data.description,
|
description=data.description,
|
||||||
color=data.color,
|
color=data.color,
|
||||||
targets=targets,
|
targets=targets,
|
||||||
devices=devices,
|
|
||||||
automations=automations,
|
|
||||||
order=store.count(),
|
order=store.count(),
|
||||||
created_at=now,
|
created_at=now,
|
||||||
updated_at=now,
|
updated_at=now,
|
||||||
@@ -199,21 +180,22 @@ async def recapture_scene_preset(
|
|||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
store: ScenePresetStore = Depends(get_scene_preset_store),
|
store: ScenePresetStore = Depends(get_scene_preset_store),
|
||||||
target_store: PictureTargetStore = Depends(get_picture_target_store),
|
target_store: PictureTargetStore = Depends(get_picture_target_store),
|
||||||
device_store: DeviceStore = Depends(get_device_store),
|
|
||||||
automation_store: AutomationStore = Depends(get_automation_store),
|
|
||||||
manager: ProcessorManager = Depends(get_processor_manager),
|
manager: ProcessorManager = Depends(get_processor_manager),
|
||||||
):
|
):
|
||||||
"""Re-capture current state into an existing preset (updates snapshot)."""
|
"""Re-capture current state into an existing preset (updates snapshot)."""
|
||||||
targets, devices, automations = capture_current_snapshot(
|
try:
|
||||||
target_store, device_store, automation_store, manager,
|
existing = store.get_preset(preset_id)
|
||||||
)
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
|
||||||
|
# Only recapture targets that are already in the preset
|
||||||
|
existing_ids = {t.target_id for t in existing.targets}
|
||||||
|
targets = capture_current_snapshot(target_store, manager, existing_ids)
|
||||||
|
|
||||||
new_snapshot = ScenePreset(
|
new_snapshot = ScenePreset(
|
||||||
id=preset_id,
|
id=preset_id,
|
||||||
name="",
|
name="",
|
||||||
targets=targets,
|
targets=targets,
|
||||||
devices=devices,
|
|
||||||
automations=automations,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -236,9 +218,6 @@ async def activate_scene_preset(
|
|||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
store: ScenePresetStore = Depends(get_scene_preset_store),
|
store: ScenePresetStore = Depends(get_scene_preset_store),
|
||||||
target_store: PictureTargetStore = Depends(get_picture_target_store),
|
target_store: PictureTargetStore = Depends(get_picture_target_store),
|
||||||
device_store: DeviceStore = Depends(get_device_store),
|
|
||||||
automation_store: AutomationStore = Depends(get_automation_store),
|
|
||||||
engine: AutomationEngine = Depends(get_automation_engine),
|
|
||||||
manager: ProcessorManager = Depends(get_processor_manager),
|
manager: ProcessorManager = Depends(get_processor_manager),
|
||||||
):
|
):
|
||||||
"""Activate a scene preset — restore the captured state."""
|
"""Activate a scene preset — restore the captured state."""
|
||||||
@@ -247,9 +226,7 @@ async def activate_scene_preset(
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
|
||||||
status, errors = await apply_scene_state(
|
status, errors = await apply_scene_state(preset, target_store, manager)
|
||||||
preset, target_store, device_store, automation_store, engine, manager,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not errors:
|
if not errors:
|
||||||
logger.info(f"Scene preset '{preset.name}' activated successfully")
|
logger.info(f"Scene preset '{preset.name}' activated successfully")
|
||||||
|
|||||||
@@ -15,22 +15,13 @@ class TargetSnapshotSchema(BaseModel):
|
|||||||
auto_start: bool = False
|
auto_start: bool = False
|
||||||
|
|
||||||
|
|
||||||
class DeviceBrightnessSnapshotSchema(BaseModel):
|
|
||||||
device_id: str
|
|
||||||
software_brightness: int = 255
|
|
||||||
|
|
||||||
|
|
||||||
class AutomationSnapshotSchema(BaseModel):
|
|
||||||
automation_id: str
|
|
||||||
enabled: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
class ScenePresetCreate(BaseModel):
|
class ScenePresetCreate(BaseModel):
|
||||||
"""Create a scene preset by capturing current state."""
|
"""Create a scene preset by capturing current state."""
|
||||||
|
|
||||||
name: str = Field(description="Preset name", min_length=1, max_length=100)
|
name: str = Field(description="Preset name", min_length=1, max_length=100)
|
||||||
description: str = Field(default="", max_length=500)
|
description: str = Field(default="", max_length=500)
|
||||||
color: str = Field(default="#4fc3f7", description="Card accent color")
|
color: str = Field(default="#4fc3f7", description="Card accent color")
|
||||||
|
target_ids: Optional[List[str]] = Field(None, description="Target IDs to capture (all if omitted)")
|
||||||
|
|
||||||
|
|
||||||
class ScenePresetUpdate(BaseModel):
|
class ScenePresetUpdate(BaseModel):
|
||||||
@@ -50,8 +41,6 @@ class ScenePresetResponse(BaseModel):
|
|||||||
description: str
|
description: str
|
||||||
color: str
|
color: str
|
||||||
targets: List[TargetSnapshotSchema]
|
targets: List[TargetSnapshotSchema]
|
||||||
devices: List[DeviceBrightnessSnapshotSchema]
|
|
||||||
automations: List[AutomationSnapshotSchema]
|
|
||||||
order: int
|
order: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
|||||||
@@ -310,22 +310,17 @@ class AutomationEngine:
|
|||||||
# For "revert" mode, capture current state before activating
|
# For "revert" mode, capture current state before activating
|
||||||
if automation.deactivation_mode == "revert":
|
if automation.deactivation_mode == "revert":
|
||||||
from wled_controller.core.scenes.scene_activator import capture_current_snapshot
|
from wled_controller.core.scenes.scene_activator import capture_current_snapshot
|
||||||
targets, devices, automations = capture_current_snapshot(
|
targets = capture_current_snapshot(self._target_store, self._manager)
|
||||||
self._target_store, self._device_store, self._store, self._manager,
|
|
||||||
)
|
|
||||||
self._pre_activation_snapshots[automation.id] = ScenePreset(
|
self._pre_activation_snapshots[automation.id] = ScenePreset(
|
||||||
id=f"_revert_{automation.id}",
|
id=f"_revert_{automation.id}",
|
||||||
name=f"Pre-activation snapshot for {automation.name}",
|
name=f"Pre-activation snapshot for {automation.name}",
|
||||||
targets=targets,
|
targets=targets,
|
||||||
devices=devices,
|
|
||||||
profiles=automations,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply the scene
|
# Apply the scene
|
||||||
from wled_controller.core.scenes.scene_activator import apply_scene_state
|
from wled_controller.core.scenes.scene_activator import apply_scene_state
|
||||||
status, errors = await apply_scene_state(
|
status, errors = await apply_scene_state(
|
||||||
preset, self._target_store, self._device_store, self._store,
|
preset, self._target_store, self._manager,
|
||||||
self, self._manager, skip_automations=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._active_automations[automation.id] = True
|
self._active_automations[automation.id] = True
|
||||||
@@ -352,11 +347,10 @@ class AutomationEngine:
|
|||||||
|
|
||||||
if deactivation_mode == "revert":
|
if deactivation_mode == "revert":
|
||||||
snapshot = self._pre_activation_snapshots.pop(automation_id, None)
|
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
|
from wled_controller.core.scenes.scene_activator import apply_scene_state
|
||||||
status, errors = await apply_scene_state(
|
status, errors = await apply_scene_state(
|
||||||
snapshot, self._target_store, self._device_store, self._store,
|
snapshot, self._target_store, self._manager,
|
||||||
self, self._manager, skip_automations=True,
|
|
||||||
)
|
)
|
||||||
if errors:
|
if errors:
|
||||||
logger.warning(f"Automation {automation_id} revert errors: {errors}")
|
logger.warning(f"Automation {automation_id} revert errors: {errors}")
|
||||||
@@ -367,13 +361,12 @@ class AutomationEngine:
|
|||||||
|
|
||||||
elif deactivation_mode == "fallback_scene":
|
elif deactivation_mode == "fallback_scene":
|
||||||
fallback_id = automation.deactivation_scene_preset_id if automation else None
|
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:
|
try:
|
||||||
fallback = self._scene_preset_store.get_preset(fallback_id)
|
fallback = self._scene_preset_store.get_preset(fallback_id)
|
||||||
from wled_controller.core.scenes.scene_activator import apply_scene_state
|
from wled_controller.core.scenes.scene_activator import apply_scene_state
|
||||||
status, errors = await apply_scene_state(
|
status, errors = await apply_scene_state(
|
||||||
fallback, self._target_store, self._device_store, self._store,
|
fallback, self._target_store, self._manager,
|
||||||
self, self._manager, skip_automations=True,
|
|
||||||
)
|
)
|
||||||
if errors:
|
if errors:
|
||||||
logger.warning(f"Automation {automation_id} fallback errors: {errors}")
|
logger.warning(f"Automation {automation_id} fallback errors: {errors}")
|
||||||
|
|||||||
@@ -3,15 +3,11 @@
|
|||||||
These functions are used by both the scene-presets API route and the automation engine.
|
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.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.picture_target_store import PictureTargetStore
|
||||||
from wled_controller.storage.automation_store import AutomationStore
|
|
||||||
from wled_controller.storage.scene_preset import (
|
from wled_controller.storage.scene_preset import (
|
||||||
AutomationSnapshot,
|
|
||||||
DeviceBrightnessSnapshot,
|
|
||||||
ScenePreset,
|
ScenePreset,
|
||||||
TargetSnapshot,
|
TargetSnapshot,
|
||||||
)
|
)
|
||||||
@@ -22,16 +18,18 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
def capture_current_snapshot(
|
def capture_current_snapshot(
|
||||||
target_store: PictureTargetStore,
|
target_store: PictureTargetStore,
|
||||||
device_store: DeviceStore,
|
|
||||||
automation_store: AutomationStore,
|
|
||||||
processor_manager: ProcessorManager,
|
processor_manager: ProcessorManager,
|
||||||
) -> Tuple[List[TargetSnapshot], List[DeviceBrightnessSnapshot], List[AutomationSnapshot]]:
|
target_ids: Optional[Set[str]] = None,
|
||||||
"""Capture current system state as snapshot lists.
|
) -> 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 = []
|
targets = []
|
||||||
for t in target_store.get_all_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)
|
proc = processor_manager._processors.get(t.id)
|
||||||
running = proc.is_running if proc else False
|
running = proc.is_running if proc else False
|
||||||
targets.append(TargetSnapshot(
|
targets.append(TargetSnapshot(
|
||||||
@@ -43,44 +41,20 @@ def capture_current_snapshot(
|
|||||||
auto_start=getattr(t, "auto_start", False),
|
auto_start=getattr(t, "auto_start", False),
|
||||||
))
|
))
|
||||||
|
|
||||||
devices = []
|
return targets
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
async def apply_scene_state(
|
async def apply_scene_state(
|
||||||
preset: ScenePreset,
|
preset: ScenePreset,
|
||||||
target_store: PictureTargetStore,
|
target_store: PictureTargetStore,
|
||||||
device_store: DeviceStore,
|
|
||||||
automation_store: AutomationStore,
|
|
||||||
automation_engine,
|
|
||||||
processor_manager: ProcessorManager,
|
processor_manager: ProcessorManager,
|
||||||
*,
|
|
||||||
skip_automations: bool = False,
|
|
||||||
) -> Tuple[str, List[str]]:
|
) -> Tuple[str, List[str]]:
|
||||||
"""Apply a scene preset's state to the system.
|
"""Apply a scene preset's state to the system.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
preset: The scene preset to activate.
|
preset: The scene preset to activate.
|
||||||
target_store: Target store for reading/updating targets.
|
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.
|
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:
|
Returns:
|
||||||
(status, errors) where status is "activated" or "partial" and
|
(status, errors) where status is "activated" or "partial" and
|
||||||
@@ -88,21 +62,7 @@ async def apply_scene_state(
|
|||||||
"""
|
"""
|
||||||
errors: List[str] = []
|
errors: List[str] = []
|
||||||
|
|
||||||
# 1. Toggle automation enable states
|
# 1. Stop targets that should be stopped
|
||||||
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
|
|
||||||
for ts in preset.targets:
|
for ts in preset.targets:
|
||||||
if not ts.running:
|
if not ts.running:
|
||||||
try:
|
try:
|
||||||
@@ -112,7 +72,7 @@ async def apply_scene_state(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(f"Stop target {ts.target_id}: {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:
|
for ts in preset.targets:
|
||||||
try:
|
try:
|
||||||
target = target_store.get_target(ts.target_id)
|
target = target_store.get_target(ts.target_id)
|
||||||
@@ -147,7 +107,7 @@ async def apply_scene_state(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(f"Target {ts.target_id} config: {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:
|
for ts in preset.targets:
|
||||||
if ts.running:
|
if ts.running:
|
||||||
try:
|
try:
|
||||||
@@ -157,28 +117,6 @@ async def apply_scene_state(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(f"Start target {ts.target_id}: {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"
|
status = "activated" if not errors else "partial"
|
||||||
if errors:
|
if errors:
|
||||||
logger.warning(f"Scene activation errors: {errors}")
|
logger.warning(f"Scene activation errors: {errors}")
|
||||||
|
|||||||
@@ -413,3 +413,27 @@ textarea:focus-visible {
|
|||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
|
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scene target selector */
|
||||||
|
.scene-target-add-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.scene-target-add-row select { flex: 1; }
|
||||||
|
.scene-target-add-row .btn { padding: 4px 10px; min-width: 0; flex: 0 0 auto; font-size: 0.85rem; }
|
||||||
|
.scene-target-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.scene-target-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
openScenePresetCapture, editScenePreset, saveScenePreset, closeScenePresetEditor,
|
openScenePresetCapture, editScenePreset, saveScenePreset, closeScenePresetEditor,
|
||||||
activateScenePreset, recaptureScenePreset, deleteScenePreset,
|
activateScenePreset, recaptureScenePreset, deleteScenePreset,
|
||||||
|
addSceneTarget, removeSceneTarget,
|
||||||
} from './features/scene-presets.js';
|
} from './features/scene-presets.js';
|
||||||
|
|
||||||
// Layer 5: device-discovery, targets
|
// Layer 5: device-discovery, targets
|
||||||
@@ -318,6 +319,8 @@ Object.assign(window, {
|
|||||||
activateScenePreset,
|
activateScenePreset,
|
||||||
recaptureScenePreset,
|
recaptureScenePreset,
|
||||||
deleteScenePreset,
|
deleteScenePreset,
|
||||||
|
addSceneTarget,
|
||||||
|
removeSceneTarget,
|
||||||
|
|
||||||
// device-discovery
|
// device-discovery
|
||||||
onDeviceTypeChanged,
|
onDeviceTypeChanged,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Modal } from '../core/modal.js';
|
|||||||
import { CardSection } from '../core/card-sections.js';
|
import { CardSection } from '../core/card-sections.js';
|
||||||
import { updateTabBadge } from './tabs.js';
|
import { updateTabBadge } from './tabs.js';
|
||||||
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
|
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
|
||||||
import { csScenes, createSceneCard } from './scene-presets.js';
|
import { csScenes, createSceneCard, updatePresetsCache } from './scene-presets.js';
|
||||||
|
|
||||||
// ===== Scene presets cache (shared by both selectors) =====
|
// ===== Scene presets cache (shared by both selectors) =====
|
||||||
let _scenesCache = [];
|
let _scenesCache = [];
|
||||||
@@ -62,6 +62,7 @@ export async function loadAutomations() {
|
|||||||
const data = await automationsResp.json();
|
const data = await automationsResp.json();
|
||||||
const scenesData = scenesResp.ok ? await scenesResp.json() : { presets: [] };
|
const scenesData = scenesResp.ok ? await scenesResp.json() : { presets: [] };
|
||||||
_scenesCache = scenesData.presets || [];
|
_scenesCache = scenesData.presets || [];
|
||||||
|
updatePresetsCache(_scenesCache);
|
||||||
|
|
||||||
// Build scene name map for card rendering
|
// Build scene name map for card rendering
|
||||||
const sceneMap = new Map(_scenesCache.map(s => [s.id, s]));
|
const sceneMap = new Map(_scenesCache.map(s => [s.id, s]));
|
||||||
@@ -208,6 +209,7 @@ export async function openAutomationEditor(automationId) {
|
|||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
_scenesCache = data.presets || [];
|
_scenesCache = data.presets || [];
|
||||||
|
updatePresetsCache(_scenesCache);
|
||||||
}
|
}
|
||||||
} catch { /* use cached */ }
|
} catch { /* use cached */ }
|
||||||
|
|
||||||
|
|||||||
@@ -9,19 +9,26 @@ import { showToast, showConfirm } from '../core/ui.js';
|
|||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
import { CardSection } from '../core/card-sections.js';
|
import { CardSection } from '../core/card-sections.js';
|
||||||
import {
|
import {
|
||||||
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET, ICON_SETTINGS,
|
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET,
|
||||||
} from '../core/icons.js';
|
} from '../core/icons.js';
|
||||||
|
|
||||||
let _presetsCache = [];
|
let _presetsCache = [];
|
||||||
let _editingId = null;
|
let _editingId = null;
|
||||||
|
let _allTargets = []; // fetched on capture open
|
||||||
|
|
||||||
|
/** Update the internal presets cache (called from automations tab after fetching). */
|
||||||
|
export function updatePresetsCache(presets) { _presetsCache = presets; }
|
||||||
|
|
||||||
class ScenePresetEditorModal extends Modal {
|
class ScenePresetEditorModal extends Modal {
|
||||||
constructor() { super('scene-preset-editor-modal'); }
|
constructor() { super('scene-preset-editor-modal'); }
|
||||||
snapshotValues() {
|
snapshotValues() {
|
||||||
|
const items = [...document.querySelectorAll('#scene-target-list .scene-target-item')]
|
||||||
|
.map(el => el.dataset.targetId).sort().join(',');
|
||||||
return {
|
return {
|
||||||
name: document.getElementById('scene-preset-editor-name').value,
|
name: document.getElementById('scene-preset-editor-name').value,
|
||||||
description: document.getElementById('scene-preset-editor-description').value,
|
description: document.getElementById('scene-preset-editor-description').value,
|
||||||
color: document.getElementById('scene-preset-editor-color').value,
|
color: document.getElementById('scene-preset-editor-color').value,
|
||||||
|
targets: items,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,14 +43,10 @@ export const csScenes = new CardSection('scenes', {
|
|||||||
|
|
||||||
export function createSceneCard(preset) {
|
export function createSceneCard(preset) {
|
||||||
const targetCount = (preset.targets || []).length;
|
const targetCount = (preset.targets || []).length;
|
||||||
const deviceCount = (preset.devices || []).length;
|
|
||||||
const automationCount = (preset.automations || []).length;
|
|
||||||
const colorStyle = `border-left: 3px solid ${escapeHtml(preset.color || '#4fc3f7')}`;
|
const colorStyle = `border-left: 3px solid ${escapeHtml(preset.color || '#4fc3f7')}`;
|
||||||
|
|
||||||
const meta = [
|
const meta = [
|
||||||
targetCount > 0 ? `${ICON_TARGET} ${targetCount} ${t('scenes.targets_count')}` : null,
|
targetCount > 0 ? `${ICON_TARGET} ${targetCount} ${t('scenes.targets_count')}` : null,
|
||||||
deviceCount > 0 ? `${ICON_SETTINGS} ${deviceCount} ${t('scenes.devices_count')}` : null,
|
|
||||||
automationCount > 0 ? `${automationCount} ${t('scenes.automations_count')}` : null,
|
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
const updated = preset.updated_at ? new Date(preset.updated_at).toLocaleString() : '';
|
const updated = preset.updated_at ? new Date(preset.updated_at).toLocaleString() : '';
|
||||||
@@ -94,13 +97,9 @@ export function renderScenePresetsSection(presets) {
|
|||||||
function _renderDashboardPresetCard(preset) {
|
function _renderDashboardPresetCard(preset) {
|
||||||
const borderStyle = `border-left: 3px solid ${escapeHtml(preset.color)}`;
|
const borderStyle = `border-left: 3px solid ${escapeHtml(preset.color)}`;
|
||||||
const targetCount = (preset.targets || []).length;
|
const targetCount = (preset.targets || []).length;
|
||||||
const deviceCount = (preset.devices || []).length;
|
|
||||||
const automationCount = (preset.automations || []).length;
|
|
||||||
|
|
||||||
const subtitle = [
|
const subtitle = [
|
||||||
targetCount > 0 ? `${targetCount} ${t('scenes.targets_count')}` : null,
|
targetCount > 0 ? `${targetCount} ${t('scenes.targets_count')}` : null,
|
||||||
deviceCount > 0 ? `${deviceCount} ${t('scenes.devices_count')}` : null,
|
|
||||||
automationCount > 0 ? `${automationCount} ${t('scenes.automations_count')}` : null,
|
|
||||||
].filter(Boolean).join(' \u00b7 ');
|
].filter(Boolean).join(' \u00b7 ');
|
||||||
|
|
||||||
return `<div class="dashboard-target dashboard-scene-preset dashboard-card-link" data-scene-id="${preset.id}" style="${borderStyle}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'scenes','data-scene-id','${preset.id}')}">
|
return `<div class="dashboard-target dashboard-scene-preset dashboard-card-link" data-scene-id="${preset.id}" style="${borderStyle}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'scenes','data-scene-id','${preset.id}')}">
|
||||||
@@ -120,7 +119,7 @@ function _renderDashboardPresetCard(preset) {
|
|||||||
|
|
||||||
// ===== Capture (create) =====
|
// ===== Capture (create) =====
|
||||||
|
|
||||||
export function openScenePresetCapture() {
|
export async function openScenePresetCapture() {
|
||||||
_editingId = null;
|
_editingId = null;
|
||||||
document.getElementById('scene-preset-editor-id').value = '';
|
document.getElementById('scene-preset-editor-id').value = '';
|
||||||
document.getElementById('scene-preset-editor-name').value = '';
|
document.getElementById('scene-preset-editor-name').value = '';
|
||||||
@@ -131,6 +130,22 @@ export function openScenePresetCapture() {
|
|||||||
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
|
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
|
||||||
if (titleEl) { titleEl.setAttribute('data-i18n', 'scenes.add'); titleEl.textContent = t('scenes.add'); }
|
if (titleEl) { titleEl.setAttribute('data-i18n', 'scenes.add'); titleEl.textContent = t('scenes.add'); }
|
||||||
|
|
||||||
|
// Fetch targets and populate selector
|
||||||
|
const selectorGroup = document.getElementById('scene-target-selector-group');
|
||||||
|
const targetList = document.getElementById('scene-target-list');
|
||||||
|
if (selectorGroup && targetList) {
|
||||||
|
selectorGroup.style.display = '';
|
||||||
|
targetList.innerHTML = '';
|
||||||
|
try {
|
||||||
|
const resp = await fetchWithAuth('/picture-targets');
|
||||||
|
if (resp.ok) {
|
||||||
|
const data = await resp.json();
|
||||||
|
_allTargets = data.targets || [];
|
||||||
|
_refreshTargetSelect();
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
scenePresetModal.open();
|
scenePresetModal.open();
|
||||||
scenePresetModal.snapshot();
|
scenePresetModal.snapshot();
|
||||||
}
|
}
|
||||||
@@ -148,6 +163,10 @@ export async function editScenePreset(presetId) {
|
|||||||
document.getElementById('scene-preset-editor-color').value = preset.color || '#4fc3f7';
|
document.getElementById('scene-preset-editor-color').value = preset.color || '#4fc3f7';
|
||||||
document.getElementById('scene-preset-editor-error').style.display = 'none';
|
document.getElementById('scene-preset-editor-error').style.display = 'none';
|
||||||
|
|
||||||
|
// Hide target selector in edit mode (metadata only)
|
||||||
|
const selectorGroup = document.getElementById('scene-target-selector-group');
|
||||||
|
if (selectorGroup) selectorGroup.style.display = 'none';
|
||||||
|
|
||||||
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
|
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
|
||||||
if (titleEl) { titleEl.setAttribute('data-i18n', 'scenes.edit'); titleEl.textContent = t('scenes.edit'); }
|
if (titleEl) { titleEl.setAttribute('data-i18n', 'scenes.edit'); titleEl.textContent = t('scenes.edit'); }
|
||||||
|
|
||||||
@@ -177,9 +196,11 @@ export async function saveScenePreset() {
|
|||||||
body: JSON.stringify({ name, description, color }),
|
body: JSON.stringify({ name, description, color }),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const target_ids = [...document.querySelectorAll('#scene-target-list .scene-target-item')]
|
||||||
|
.map(el => el.dataset.targetId);
|
||||||
resp = await fetchWithAuth('/scene-presets', {
|
resp = await fetchWithAuth('/scene-presets', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ name, description, color }),
|
body: JSON.stringify({ name, description, color, target_ids }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +225,49 @@ export async function closeScenePresetEditor() {
|
|||||||
await scenePresetModal.close();
|
await scenePresetModal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Target selector helpers =====
|
||||||
|
|
||||||
|
function _refreshTargetSelect() {
|
||||||
|
const select = document.getElementById('scene-target-select');
|
||||||
|
if (!select) return;
|
||||||
|
const added = new Set(
|
||||||
|
[...document.querySelectorAll('#scene-target-list .scene-target-item')]
|
||||||
|
.map(el => el.dataset.targetId)
|
||||||
|
);
|
||||||
|
select.innerHTML = '';
|
||||||
|
for (const tgt of _allTargets) {
|
||||||
|
if (added.has(tgt.id)) continue;
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = tgt.id;
|
||||||
|
opt.textContent = tgt.name;
|
||||||
|
select.appendChild(opt);
|
||||||
|
}
|
||||||
|
// Disable add button when no targets available
|
||||||
|
const addBtn = select.parentElement?.querySelector('button');
|
||||||
|
if (addBtn) addBtn.disabled = select.options.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSceneTarget() {
|
||||||
|
const select = document.getElementById('scene-target-select');
|
||||||
|
const list = document.getElementById('scene-target-list');
|
||||||
|
if (!select || !list || !select.value) return;
|
||||||
|
|
||||||
|
const targetId = select.value;
|
||||||
|
const targetName = select.options[select.selectedIndex].text;
|
||||||
|
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'scene-target-item';
|
||||||
|
item.dataset.targetId = targetId;
|
||||||
|
item.innerHTML = `<span>${escapeHtml(targetName)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">✕</button>`;
|
||||||
|
list.appendChild(item);
|
||||||
|
_refreshTargetSelect();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeSceneTarget(btn) {
|
||||||
|
btn.closest('.scene-target-item').remove();
|
||||||
|
_refreshTargetSelect();
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Activate =====
|
// ===== Activate =====
|
||||||
|
|
||||||
export async function activateScenePreset(presetId) {
|
export async function activateScenePreset(presetId) {
|
||||||
|
|||||||
@@ -630,13 +630,13 @@
|
|||||||
"scenes.description.hint": "Optional description of what this scene does",
|
"scenes.description.hint": "Optional description of what this scene does",
|
||||||
"scenes.color": "Card Color:",
|
"scenes.color": "Card Color:",
|
||||||
"scenes.color.hint": "Accent color for the scene card on the dashboard",
|
"scenes.color.hint": "Accent color for the scene card on the dashboard",
|
||||||
|
"scenes.targets": "Targets:",
|
||||||
|
"scenes.targets.hint": "Select which targets to include in this scene snapshot",
|
||||||
"scenes.capture": "Capture",
|
"scenes.capture": "Capture",
|
||||||
"scenes.activate": "Activate scene",
|
"scenes.activate": "Activate scene",
|
||||||
"scenes.recapture": "Recapture current state",
|
"scenes.recapture": "Recapture current state",
|
||||||
"scenes.delete": "Delete scene",
|
"scenes.delete": "Delete scene",
|
||||||
"scenes.targets_count": "targets",
|
"scenes.targets_count": "targets",
|
||||||
"scenes.devices_count": "devices",
|
|
||||||
"scenes.automations_count": "automations",
|
|
||||||
"scenes.captured": "Scene captured",
|
"scenes.captured": "Scene captured",
|
||||||
"scenes.updated": "Scene updated",
|
"scenes.updated": "Scene updated",
|
||||||
"scenes.activated": "Scene activated",
|
"scenes.activated": "Scene activated",
|
||||||
|
|||||||
@@ -630,13 +630,13 @@
|
|||||||
"scenes.description.hint": "Необязательное описание назначения этой сцены",
|
"scenes.description.hint": "Необязательное описание назначения этой сцены",
|
||||||
"scenes.color": "Цвет карточки:",
|
"scenes.color": "Цвет карточки:",
|
||||||
"scenes.color.hint": "Акцентный цвет для карточки сцены на панели управления",
|
"scenes.color.hint": "Акцентный цвет для карточки сцены на панели управления",
|
||||||
|
"scenes.targets": "Цели:",
|
||||||
|
"scenes.targets.hint": "Выберите какие цели включить в снимок сцены",
|
||||||
"scenes.capture": "Захват",
|
"scenes.capture": "Захват",
|
||||||
"scenes.activate": "Активировать сцену",
|
"scenes.activate": "Активировать сцену",
|
||||||
"scenes.recapture": "Перезахватить текущее состояние",
|
"scenes.recapture": "Перезахватить текущее состояние",
|
||||||
"scenes.delete": "Удалить сцену",
|
"scenes.delete": "Удалить сцену",
|
||||||
"scenes.targets_count": "целей",
|
"scenes.targets_count": "целей",
|
||||||
"scenes.devices_count": "устройств",
|
|
||||||
"scenes.automations_count": "автоматизаций",
|
|
||||||
"scenes.captured": "Сцена захвачена",
|
"scenes.captured": "Сцена захвачена",
|
||||||
"scenes.updated": "Сцена обновлена",
|
"scenes.updated": "Сцена обновлена",
|
||||||
"scenes.activated": "Сцена активирована",
|
"scenes.activated": "Сцена активирована",
|
||||||
|
|||||||
@@ -630,13 +630,13 @@
|
|||||||
"scenes.description.hint": "此场景功能的可选描述",
|
"scenes.description.hint": "此场景功能的可选描述",
|
||||||
"scenes.color": "卡片颜色:",
|
"scenes.color": "卡片颜色:",
|
||||||
"scenes.color.hint": "仪表盘上场景卡片的强调色",
|
"scenes.color.hint": "仪表盘上场景卡片的强调色",
|
||||||
|
"scenes.targets": "目标:",
|
||||||
|
"scenes.targets.hint": "选择要包含在此场景快照中的目标",
|
||||||
"scenes.capture": "捕获",
|
"scenes.capture": "捕获",
|
||||||
"scenes.activate": "激活场景",
|
"scenes.activate": "激活场景",
|
||||||
"scenes.recapture": "重新捕获当前状态",
|
"scenes.recapture": "重新捕获当前状态",
|
||||||
"scenes.delete": "删除场景",
|
"scenes.delete": "删除场景",
|
||||||
"scenes.targets_count": "目标",
|
"scenes.targets_count": "目标",
|
||||||
"scenes.devices_count": "设备",
|
|
||||||
"scenes.automations_count": "自动化",
|
|
||||||
"scenes.captured": "场景已捕获",
|
"scenes.captured": "场景已捕获",
|
||||||
"scenes.updated": "场景已更新",
|
"scenes.updated": "场景已更新",
|
||||||
"scenes.activated": "场景已激活",
|
"scenes.activated": "场景已激活",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""Scene preset data models — snapshot of current system state."""
|
"""Scene preset data models — snapshot of target state."""
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -38,59 +38,15 @@ class TargetSnapshot:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DeviceBrightnessSnapshot:
|
|
||||||
"""Snapshot of a device's software brightness."""
|
|
||||||
|
|
||||||
device_id: str
|
|
||||||
software_brightness: int = 255
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
|
||||||
return {
|
|
||||||
"device_id": self.device_id,
|
|
||||||
"software_brightness": self.software_brightness,
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: dict) -> "DeviceBrightnessSnapshot":
|
|
||||||
return cls(
|
|
||||||
device_id=data["device_id"],
|
|
||||||
software_brightness=data.get("software_brightness", 255),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AutomationSnapshot:
|
|
||||||
"""Snapshot of an automation's enabled state."""
|
|
||||||
|
|
||||||
automation_id: str
|
|
||||||
enabled: bool = True
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
|
||||||
return {
|
|
||||||
"automation_id": self.automation_id,
|
|
||||||
"enabled": self.enabled,
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: dict) -> "AutomationSnapshot":
|
|
||||||
return cls(
|
|
||||||
automation_id=data.get("automation_id", ""),
|
|
||||||
enabled=data.get("enabled", True),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ScenePreset:
|
class ScenePreset:
|
||||||
"""A named snapshot of system state that can be restored."""
|
"""A named snapshot of target state that can be restored."""
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
description: str = ""
|
description: str = ""
|
||||||
color: str = "#4fc3f7" # accent color for the card
|
color: str = "#4fc3f7" # accent color for the card
|
||||||
targets: List[TargetSnapshot] = field(default_factory=list)
|
targets: List[TargetSnapshot] = field(default_factory=list)
|
||||||
devices: List[DeviceBrightnessSnapshot] = field(default_factory=list)
|
|
||||||
automations: List[AutomationSnapshot] = field(default_factory=list)
|
|
||||||
order: int = 0
|
order: int = 0
|
||||||
created_at: datetime = field(default_factory=datetime.utcnow)
|
created_at: datetime = field(default_factory=datetime.utcnow)
|
||||||
updated_at: datetime = field(default_factory=datetime.utcnow)
|
updated_at: datetime = field(default_factory=datetime.utcnow)
|
||||||
@@ -102,8 +58,6 @@ class ScenePreset:
|
|||||||
"description": self.description,
|
"description": self.description,
|
||||||
"color": self.color,
|
"color": self.color,
|
||||||
"targets": [t.to_dict() for t in self.targets],
|
"targets": [t.to_dict() for t in self.targets],
|
||||||
"devices": [d.to_dict() for d in self.devices],
|
|
||||||
"automations": [a.to_dict() for a in self.automations],
|
|
||||||
"order": self.order,
|
"order": self.order,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
"updated_at": self.updated_at.isoformat(),
|
"updated_at": self.updated_at.isoformat(),
|
||||||
@@ -117,8 +71,6 @@ class ScenePreset:
|
|||||||
description=data.get("description", ""),
|
description=data.get("description", ""),
|
||||||
color=data.get("color", "#4fc3f7"),
|
color=data.get("color", "#4fc3f7"),
|
||||||
targets=[TargetSnapshot.from_dict(t) for t in data.get("targets", [])],
|
targets=[TargetSnapshot.from_dict(t) for t in data.get("targets", [])],
|
||||||
devices=[DeviceBrightnessSnapshot.from_dict(d) for d in data.get("devices", [])],
|
|
||||||
automations=[AutomationSnapshot.from_dict(a) for a in data.get("automations", [])],
|
|
||||||
order=data.get("order", 0),
|
order=data.get("order", 0),
|
||||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
||||||
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
|
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
|
||||||
|
|||||||
@@ -115,8 +115,6 @@ class ScenePresetStore:
|
|||||||
|
|
||||||
existing = self._presets[preset_id]
|
existing = self._presets[preset_id]
|
||||||
existing.targets = preset.targets
|
existing.targets = preset.targets
|
||||||
existing.devices = preset.devices
|
|
||||||
existing.profiles = preset.profiles
|
|
||||||
existing.updated_at = datetime.utcnow()
|
existing.updated_at = datetime.utcnow()
|
||||||
self._save()
|
self._save()
|
||||||
logger.info(f"Recaptured scene preset: {preset_id}")
|
logger.info(f"Recaptured scene preset: {preset_id}")
|
||||||
|
|||||||
@@ -36,6 +36,19 @@
|
|||||||
<input type="color" id="scene-preset-editor-color" value="#4fc3f7">
|
<input type="color" id="scene-preset-editor-color" value="#4fc3f7">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="scene-target-selector-group" style="display:none">
|
||||||
|
<div class="label-row">
|
||||||
|
<label data-i18n="scenes.targets">Targets:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="scenes.targets.hint">Select which targets to include in this scene snapshot</small>
|
||||||
|
<div class="scene-target-add-row">
|
||||||
|
<select id="scene-target-select"></select>
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" onclick="addSceneTarget()">+</button>
|
||||||
|
</div>
|
||||||
|
<div id="scene-target-list" class="scene-target-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="scene-preset-editor-error" class="error-message" style="display: none;"></div>
|
<div id="scene-preset-editor-error" class="error-message" style="display: none;"></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user