Rename profiles to automations across backend and frontend
Rename the "profiles" entity to "automations" throughout the entire codebase for clarity. Updates Python models, storage, API routes/schemas, engine, frontend JS modules, HTML templates, CSS classes, i18n keys (en/ru/zh), dashboard, tutorials, and command palette. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1
server/src/wled_controller/core/automations/__init__.py
Normal file
1
server/src/wled_controller/core/automations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Automation engine — condition evaluation and scene activation."""
|
||||
@@ -1,34 +1,34 @@
|
||||
"""Profile engine — background loop that evaluates conditions and activates scenes."""
|
||||
"""Automation engine — background loop that evaluates conditions and activates scenes."""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
from wled_controller.core.profiles.platform_detector import PlatformDetector
|
||||
from wled_controller.storage.profile import (
|
||||
from wled_controller.core.automations.platform_detector import PlatformDetector
|
||||
from wled_controller.storage.automation import (
|
||||
AlwaysCondition,
|
||||
ApplicationCondition,
|
||||
Automation,
|
||||
Condition,
|
||||
DisplayStateCondition,
|
||||
MQTTCondition,
|
||||
Profile,
|
||||
SystemIdleCondition,
|
||||
TimeOfDayCondition,
|
||||
)
|
||||
from wled_controller.storage.profile_store import ProfileStore
|
||||
from wled_controller.storage.automation_store import AutomationStore
|
||||
from wled_controller.storage.scene_preset import ScenePreset
|
||||
from wled_controller.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ProfileEngine:
|
||||
"""Evaluates profile conditions and activates/deactivates scene presets."""
|
||||
class AutomationEngine:
|
||||
"""Evaluates automation conditions and activates/deactivates scene presets."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
profile_store: ProfileStore,
|
||||
automation_store: AutomationStore,
|
||||
processor_manager,
|
||||
poll_interval: float = 1.0,
|
||||
mqtt_service=None,
|
||||
@@ -36,7 +36,7 @@ class ProfileEngine:
|
||||
target_store=None,
|
||||
device_store=None,
|
||||
):
|
||||
self._store = profile_store
|
||||
self._store = automation_store
|
||||
self._manager = processor_manager
|
||||
self._poll_interval = poll_interval
|
||||
self._detector = PlatformDetector()
|
||||
@@ -48,11 +48,11 @@ class ProfileEngine:
|
||||
self._eval_lock = asyncio.Lock()
|
||||
|
||||
# Runtime state (not persisted)
|
||||
# profile_id → True when profile is currently active
|
||||
self._active_profiles: Dict[str, bool] = {}
|
||||
# profile_id → snapshot captured before activation (for "revert" mode)
|
||||
# automation_id → True when automation is currently active
|
||||
self._active_automations: Dict[str, bool] = {}
|
||||
# automation_id → snapshot captured before activation (for "revert" mode)
|
||||
self._pre_activation_snapshots: Dict[str, ScenePreset] = {}
|
||||
# profile_id → datetime of last activation / deactivation
|
||||
# automation_id → datetime of last activation / deactivation
|
||||
self._last_activated: Dict[str, datetime] = {}
|
||||
self._last_deactivated: Dict[str, datetime] = {}
|
||||
|
||||
@@ -60,7 +60,7 @@ class ProfileEngine:
|
||||
if self._task is not None:
|
||||
return
|
||||
self._task = asyncio.create_task(self._poll_loop())
|
||||
logger.info("Profile engine started")
|
||||
logger.info("Automation engine started")
|
||||
|
||||
async def stop(self) -> None:
|
||||
if self._task is None:
|
||||
@@ -73,11 +73,11 @@ class ProfileEngine:
|
||||
pass
|
||||
self._task = None
|
||||
|
||||
# Deactivate all profiles (stop owned targets)
|
||||
for profile_id in list(self._active_profiles.keys()):
|
||||
await self._deactivate_profile(profile_id)
|
||||
# Deactivate all automations
|
||||
for automation_id in list(self._active_automations.keys()):
|
||||
await self._deactivate_automation(automation_id)
|
||||
|
||||
logger.info("Profile engine stopped")
|
||||
logger.info("Automation engine stopped")
|
||||
|
||||
async def _poll_loop(self) -> None:
|
||||
try:
|
||||
@@ -85,7 +85,7 @@ class ProfileEngine:
|
||||
try:
|
||||
await self._evaluate_all()
|
||||
except Exception as e:
|
||||
logger.error(f"Profile evaluation error: {e}", exc_info=True)
|
||||
logger.error(f"Automation evaluation error: {e}", exc_info=True)
|
||||
await asyncio.sleep(self._poll_interval)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
@@ -115,20 +115,20 @@ class ProfileEngine:
|
||||
return running_procs, topmost_proc, topmost_fullscreen, fullscreen_procs, idle_seconds, display_state
|
||||
|
||||
async def _evaluate_all_locked(self) -> None:
|
||||
profiles = self._store.get_all_profiles()
|
||||
if not profiles:
|
||||
# No profiles — deactivate any stale state
|
||||
for pid in list(self._active_profiles.keys()):
|
||||
await self._deactivate_profile(pid)
|
||||
automations = self._store.get_all_automations()
|
||||
if not automations:
|
||||
# No automations — deactivate any stale state
|
||||
for aid in list(self._active_automations.keys()):
|
||||
await self._deactivate_automation(aid)
|
||||
return
|
||||
|
||||
# Determine which detection methods are actually needed
|
||||
match_types_used: set = set()
|
||||
needs_idle = False
|
||||
needs_display_state = False
|
||||
for p in profiles:
|
||||
if p.enabled:
|
||||
for c in p.conditions:
|
||||
for a in automations:
|
||||
if a.enabled:
|
||||
for c in a.conditions:
|
||||
if isinstance(c, ApplicationCondition):
|
||||
match_types_used.add(c.match_type)
|
||||
elif isinstance(c, SystemIdleCondition):
|
||||
@@ -151,34 +151,34 @@ class ProfileEngine:
|
||||
)
|
||||
)
|
||||
|
||||
active_profile_ids = set()
|
||||
active_automation_ids = set()
|
||||
|
||||
for profile in profiles:
|
||||
for automation in automations:
|
||||
should_be_active = (
|
||||
profile.enabled
|
||||
and (len(profile.conditions) == 0
|
||||
automation.enabled
|
||||
and (len(automation.conditions) == 0
|
||||
or self._evaluate_conditions(
|
||||
profile, running_procs, topmost_proc, topmost_fullscreen,
|
||||
automation, running_procs, topmost_proc, topmost_fullscreen,
|
||||
fullscreen_procs, idle_seconds, display_state))
|
||||
)
|
||||
|
||||
is_active = profile.id in self._active_profiles
|
||||
is_active = automation.id in self._active_automations
|
||||
|
||||
if should_be_active and not is_active:
|
||||
await self._activate_profile(profile)
|
||||
active_profile_ids.add(profile.id)
|
||||
await self._activate_automation(automation)
|
||||
active_automation_ids.add(automation.id)
|
||||
elif should_be_active and is_active:
|
||||
active_profile_ids.add(profile.id)
|
||||
active_automation_ids.add(automation.id)
|
||||
elif not should_be_active and is_active:
|
||||
await self._deactivate_profile(profile.id)
|
||||
await self._deactivate_automation(automation.id)
|
||||
|
||||
# Deactivate profiles that were removed from store while active
|
||||
for pid in list(self._active_profiles.keys()):
|
||||
if pid not in active_profile_ids:
|
||||
await self._deactivate_profile(pid)
|
||||
# Deactivate automations that were removed from store while active
|
||||
for aid in list(self._active_automations.keys()):
|
||||
if aid not in active_automation_ids:
|
||||
await self._deactivate_automation(aid)
|
||||
|
||||
def _evaluate_conditions(
|
||||
self, profile: Profile, running_procs: Set[str],
|
||||
self, automation: Automation, running_procs: Set[str],
|
||||
topmost_proc: Optional[str], topmost_fullscreen: bool,
|
||||
fullscreen_procs: Set[str],
|
||||
idle_seconds: Optional[float], display_state: Optional[str],
|
||||
@@ -188,10 +188,10 @@ class ProfileEngine:
|
||||
c, running_procs, topmost_proc, topmost_fullscreen,
|
||||
fullscreen_procs, idle_seconds, display_state,
|
||||
)
|
||||
for c in profile.conditions
|
||||
for c in automation.conditions
|
||||
]
|
||||
|
||||
if profile.condition_logic == "and":
|
||||
if automation.condition_logic == "and":
|
||||
return all(results)
|
||||
return any(results) # "or" is default
|
||||
|
||||
@@ -287,116 +287,116 @@ class ProfileEngine:
|
||||
# Default: "running"
|
||||
return any(app in running_procs for app in apps_lower)
|
||||
|
||||
async def _activate_profile(self, profile: Profile) -> None:
|
||||
if not profile.scene_preset_id:
|
||||
async def _activate_automation(self, automation: Automation) -> None:
|
||||
if not automation.scene_preset_id:
|
||||
# No scene configured — just mark active (conditions matched but nothing to do)
|
||||
self._active_profiles[profile.id] = True
|
||||
self._last_activated[profile.id] = datetime.now(timezone.utc)
|
||||
self._fire_event(profile.id, "activated")
|
||||
logger.info(f"Profile '{profile.name}' activated (no scene configured)")
|
||||
self._active_automations[automation.id] = True
|
||||
self._last_activated[automation.id] = datetime.now(timezone.utc)
|
||||
self._fire_event(automation.id, "activated")
|
||||
logger.info(f"Automation '{automation.name}' activated (no scene configured)")
|
||||
return
|
||||
|
||||
if not self._scene_preset_store or not self._target_store or not self._device_store:
|
||||
logger.warning(f"Profile '{profile.name}' matched but scene stores not available")
|
||||
logger.warning(f"Automation '{automation.name}' matched but scene stores not available")
|
||||
return
|
||||
|
||||
# Load the scene preset
|
||||
try:
|
||||
preset = self._scene_preset_store.get_preset(profile.scene_preset_id)
|
||||
preset = self._scene_preset_store.get_preset(automation.scene_preset_id)
|
||||
except ValueError:
|
||||
logger.warning(f"Profile '{profile.name}': scene preset {profile.scene_preset_id} not found")
|
||||
logger.warning(f"Automation '{automation.name}': scene preset {automation.scene_preset_id} not found")
|
||||
return
|
||||
|
||||
# For "revert" mode, capture current state before activating
|
||||
if profile.deactivation_mode == "revert":
|
||||
if automation.deactivation_mode == "revert":
|
||||
from wled_controller.core.scenes.scene_activator import capture_current_snapshot
|
||||
targets, devices, profiles = capture_current_snapshot(
|
||||
targets, devices, automations = capture_current_snapshot(
|
||||
self._target_store, self._device_store, self._store, self._manager,
|
||||
)
|
||||
self._pre_activation_snapshots[profile.id] = ScenePreset(
|
||||
id=f"_revert_{profile.id}",
|
||||
name=f"Pre-activation snapshot for {profile.name}",
|
||||
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=profiles,
|
||||
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_profiles=True,
|
||||
self, self._manager, skip_automations=True,
|
||||
)
|
||||
|
||||
self._active_profiles[profile.id] = True
|
||||
self._last_activated[profile.id] = datetime.now(timezone.utc)
|
||||
self._fire_event(profile.id, "activated")
|
||||
self._active_automations[automation.id] = True
|
||||
self._last_activated[automation.id] = datetime.now(timezone.utc)
|
||||
self._fire_event(automation.id, "activated")
|
||||
|
||||
if errors:
|
||||
logger.warning(f"Profile '{profile.name}' activated with errors: {errors}")
|
||||
logger.warning(f"Automation '{automation.name}' activated with errors: {errors}")
|
||||
else:
|
||||
logger.info(f"Profile '{profile.name}' activated (scene '{preset.name}' applied)")
|
||||
logger.info(f"Automation '{automation.name}' activated (scene '{preset.name}' applied)")
|
||||
|
||||
async def _deactivate_profile(self, profile_id: str) -> None:
|
||||
was_active = self._active_profiles.pop(profile_id, False)
|
||||
async def _deactivate_automation(self, automation_id: str) -> None:
|
||||
was_active = self._active_automations.pop(automation_id, False)
|
||||
if not was_active:
|
||||
return
|
||||
|
||||
# Look up the profile for deactivation settings
|
||||
# Look up the automation for deactivation settings
|
||||
try:
|
||||
profile = self._store.get_profile(profile_id)
|
||||
automation = self._store.get_automation(automation_id)
|
||||
except ValueError:
|
||||
profile = None
|
||||
automation = None
|
||||
|
||||
deactivation_mode = profile.deactivation_mode if profile else "none"
|
||||
deactivation_mode = automation.deactivation_mode if automation else "none"
|
||||
|
||||
if deactivation_mode == "revert":
|
||||
snapshot = self._pre_activation_snapshots.pop(profile_id, None)
|
||||
snapshot = self._pre_activation_snapshots.pop(automation_id, None)
|
||||
if snapshot and self._target_store and self._device_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_profiles=True,
|
||||
self, self._manager, skip_automations=True,
|
||||
)
|
||||
if errors:
|
||||
logger.warning(f"Profile {profile_id} revert errors: {errors}")
|
||||
logger.warning(f"Automation {automation_id} revert errors: {errors}")
|
||||
else:
|
||||
logger.info(f"Profile {profile_id} deactivated (reverted to previous state)")
|
||||
logger.info(f"Automation {automation_id} deactivated (reverted to previous state)")
|
||||
else:
|
||||
logger.warning(f"Profile {profile_id}: no snapshot available for revert")
|
||||
logger.warning(f"Automation {automation_id}: no snapshot available for revert")
|
||||
|
||||
elif deactivation_mode == "fallback_scene":
|
||||
fallback_id = profile.deactivation_scene_preset_id if profile 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:
|
||||
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_profiles=True,
|
||||
self, self._manager, skip_automations=True,
|
||||
)
|
||||
if errors:
|
||||
logger.warning(f"Profile {profile_id} fallback errors: {errors}")
|
||||
logger.warning(f"Automation {automation_id} fallback errors: {errors}")
|
||||
else:
|
||||
logger.info(f"Profile {profile_id} deactivated (fallback scene '{fallback.name}' applied)")
|
||||
logger.info(f"Automation {automation_id} deactivated (fallback scene '{fallback.name}' applied)")
|
||||
except ValueError:
|
||||
logger.warning(f"Profile {profile_id}: fallback scene {fallback_id} not found")
|
||||
logger.warning(f"Automation {automation_id}: fallback scene {fallback_id} not found")
|
||||
else:
|
||||
logger.info(f"Profile {profile_id} deactivated (no fallback scene configured)")
|
||||
logger.info(f"Automation {automation_id} deactivated (no fallback scene configured)")
|
||||
else:
|
||||
# "none" mode — just clear active state
|
||||
logger.info(f"Profile {profile_id} deactivated")
|
||||
logger.info(f"Automation {automation_id} deactivated")
|
||||
|
||||
self._last_deactivated[profile_id] = datetime.now(timezone.utc)
|
||||
self._fire_event(profile_id, "deactivated")
|
||||
self._last_deactivated[automation_id] = datetime.now(timezone.utc)
|
||||
self._fire_event(automation_id, "deactivated")
|
||||
# Clean up any leftover snapshot
|
||||
self._pre_activation_snapshots.pop(profile_id, None)
|
||||
self._pre_activation_snapshots.pop(automation_id, None)
|
||||
|
||||
def _fire_event(self, profile_id: str, action: str) -> None:
|
||||
def _fire_event(self, automation_id: str, action: str) -> None:
|
||||
try:
|
||||
self._manager._fire_event({
|
||||
"type": "profile_state_changed",
|
||||
"profile_id": profile_id,
|
||||
"type": "automation_state_changed",
|
||||
"automation_id": automation_id,
|
||||
"action": action,
|
||||
})
|
||||
except Exception:
|
||||
@@ -404,30 +404,30 @@ class ProfileEngine:
|
||||
|
||||
# ===== Public query methods (used by API) =====
|
||||
|
||||
def get_profile_state(self, profile_id: str) -> dict:
|
||||
"""Get runtime state of a single profile."""
|
||||
is_active = profile_id in self._active_profiles
|
||||
def get_automation_state(self, automation_id: str) -> dict:
|
||||
"""Get runtime state of a single automation."""
|
||||
is_active = automation_id in self._active_automations
|
||||
return {
|
||||
"is_active": is_active,
|
||||
"last_activated_at": self._last_activated.get(profile_id),
|
||||
"last_deactivated_at": self._last_deactivated.get(profile_id),
|
||||
"last_activated_at": self._last_activated.get(automation_id),
|
||||
"last_deactivated_at": self._last_deactivated.get(automation_id),
|
||||
}
|
||||
|
||||
def get_all_profile_states(self) -> Dict[str, dict]:
|
||||
"""Get runtime states of all profiles."""
|
||||
def get_all_automation_states(self) -> Dict[str, dict]:
|
||||
"""Get runtime states of all automations."""
|
||||
result = {}
|
||||
for profile in self._store.get_all_profiles():
|
||||
result[profile.id] = self.get_profile_state(profile.id)
|
||||
for automation in self._store.get_all_automations():
|
||||
result[automation.id] = self.get_automation_state(automation.id)
|
||||
return result
|
||||
|
||||
async def trigger_evaluate(self) -> None:
|
||||
"""Run a single evaluation cycle immediately (used after enabling a profile)."""
|
||||
"""Run a single evaluation cycle immediately (used after enabling an automation)."""
|
||||
try:
|
||||
await self._evaluate_all()
|
||||
except Exception as e:
|
||||
logger.error(f"Immediate profile evaluation error: {e}", exc_info=True)
|
||||
logger.error(f"Immediate automation evaluation error: {e}", exc_info=True)
|
||||
|
||||
async def deactivate_if_active(self, profile_id: str) -> None:
|
||||
"""Deactivate a profile immediately (used when disabling/deleting)."""
|
||||
if profile_id in self._active_profiles:
|
||||
await self._deactivate_profile(profile_id)
|
||||
async def deactivate_if_active(self, automation_id: str) -> None:
|
||||
"""Deactivate an automation immediately (used when disabling/deleting)."""
|
||||
if automation_id in self._active_automations:
|
||||
await self._deactivate_automation(automation_id)
|
||||
@@ -18,7 +18,7 @@ class MQTTService:
|
||||
Features:
|
||||
- Publish messages (retained or transient)
|
||||
- Subscribe to topics with callback dispatch
|
||||
- Topic value cache for synchronous reads (profile condition evaluation)
|
||||
- Topic value cache for synchronous reads (automation condition evaluation)
|
||||
- Auto-reconnect loop
|
||||
- Birth / will messages for online status
|
||||
"""
|
||||
@@ -95,7 +95,7 @@ class MQTTService:
|
||||
logger.warning(f"MQTT subscribe failed ({topic}): {e}")
|
||||
|
||||
def get_last_value(self, topic: str) -> Optional[str]:
|
||||
"""Get cached last value for a topic (synchronous — for profile evaluation)."""
|
||||
"""Get cached last value for a topic (synchronous — for automation evaluation)."""
|
||||
return self._topic_cache.get(topic)
|
||||
|
||||
async def _connection_loop(self) -> None:
|
||||
@@ -170,7 +170,7 @@ class MQTTService:
|
||||
topic = f"{self._config.base_topic}/target/{target_id}/state"
|
||||
await self.publish(topic, json.dumps(state), retain=True)
|
||||
|
||||
async def publish_profile_state(self, profile_id: str, action: str) -> None:
|
||||
"""Publish profile state change to MQTT."""
|
||||
topic = f"{self._config.base_topic}/profile/{profile_id}/state"
|
||||
async def publish_automation_state(self, automation_id: str, action: str) -> None:
|
||||
"""Publish automation state change to MQTT."""
|
||||
topic = f"{self._config.base_topic}/automation/{automation_id}/state"
|
||||
await self.publish(topic, json.dumps({"action": action}), retain=True)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Profile automation — condition evaluation and target management."""
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Reusable scene activation and snapshot capture logic.
|
||||
|
||||
These functions are used by both the scene-presets API route and the profile engine.
|
||||
These functions are used by both the scene-presets API route and the automation engine.
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
@@ -8,10 +8,10 @@ from typing import List, Optional, 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.profile_store import ProfileStore
|
||||
from wled_controller.storage.automation_store import AutomationStore
|
||||
from wled_controller.storage.scene_preset import (
|
||||
AutomationSnapshot,
|
||||
DeviceBrightnessSnapshot,
|
||||
ProfileSnapshot,
|
||||
ScenePreset,
|
||||
TargetSnapshot,
|
||||
)
|
||||
@@ -23,12 +23,12 @@ logger = get_logger(__name__)
|
||||
def capture_current_snapshot(
|
||||
target_store: PictureTargetStore,
|
||||
device_store: DeviceStore,
|
||||
profile_store: ProfileStore,
|
||||
automation_store: AutomationStore,
|
||||
processor_manager: ProcessorManager,
|
||||
) -> Tuple[List[TargetSnapshot], List[DeviceBrightnessSnapshot], List[ProfileSnapshot]]:
|
||||
) -> Tuple[List[TargetSnapshot], List[DeviceBrightnessSnapshot], List[AutomationSnapshot]]:
|
||||
"""Capture current system state as snapshot lists.
|
||||
|
||||
Returns (targets, devices, profiles) snapshot tuples.
|
||||
Returns (targets, devices, automations) snapshot tuples.
|
||||
"""
|
||||
targets = []
|
||||
for t in target_store.get_all_targets():
|
||||
@@ -50,25 +50,25 @@ def capture_current_snapshot(
|
||||
software_brightness=getattr(d, "software_brightness", 255),
|
||||
))
|
||||
|
||||
profiles = []
|
||||
for p in profile_store.get_all_profiles():
|
||||
profiles.append(ProfileSnapshot(
|
||||
profile_id=p.id,
|
||||
enabled=p.enabled,
|
||||
automations = []
|
||||
for a in automation_store.get_all_automations():
|
||||
automations.append(AutomationSnapshot(
|
||||
automation_id=a.id,
|
||||
enabled=a.enabled,
|
||||
))
|
||||
|
||||
return targets, devices, profiles
|
||||
return targets, devices, automations
|
||||
|
||||
|
||||
async def apply_scene_state(
|
||||
preset: ScenePreset,
|
||||
target_store: PictureTargetStore,
|
||||
device_store: DeviceStore,
|
||||
profile_store: ProfileStore,
|
||||
profile_engine,
|
||||
automation_store: AutomationStore,
|
||||
automation_engine,
|
||||
processor_manager: ProcessorManager,
|
||||
*,
|
||||
skip_profiles: bool = False,
|
||||
skip_automations: bool = False,
|
||||
) -> Tuple[str, List[str]]:
|
||||
"""Apply a scene preset's state to the system.
|
||||
|
||||
@@ -76,11 +76,11 @@ async def apply_scene_state(
|
||||
preset: The scene preset to activate.
|
||||
target_store: Target store for reading/updating targets.
|
||||
device_store: Device store for reading/updating devices.
|
||||
profile_store: Profile store for reading/updating profiles.
|
||||
profile_engine: Profile engine for deactivation and re-evaluation.
|
||||
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_profiles: If True, skip toggling profile enable states (used when
|
||||
called from the profile engine itself to avoid recursion).
|
||||
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,19 +88,19 @@ async def apply_scene_state(
|
||||
"""
|
||||
errors: List[str] = []
|
||||
|
||||
# 1. Toggle profile enable states
|
||||
if not skip_profiles:
|
||||
for ps in preset.profiles:
|
||||
# 1. Toggle automation enable states
|
||||
if not skip_automations:
|
||||
for auto_snap in preset.automations:
|
||||
try:
|
||||
p = profile_store.get_profile(ps.profile_id)
|
||||
if p.enabled != ps.enabled:
|
||||
if not ps.enabled:
|
||||
await profile_engine.deactivate_if_active(ps.profile_id)
|
||||
profile_store.update_profile(ps.profile_id, enabled=ps.enabled)
|
||||
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"Profile {ps.profile_id} not found (skipped)")
|
||||
errors.append(f"Automation {auto_snap.automation_id} not found (skipped)")
|
||||
except Exception as e:
|
||||
errors.append(f"Profile {ps.profile_id}: {e}")
|
||||
errors.append(f"Automation {auto_snap.automation_id}: {e}")
|
||||
|
||||
# 2. Stop targets that should be stopped
|
||||
for ts in preset.targets:
|
||||
@@ -172,12 +172,12 @@ async def apply_scene_state(
|
||||
except Exception as e:
|
||||
errors.append(f"Device {ds.device_id} brightness: {e}")
|
||||
|
||||
# Trigger profile re-evaluation after all changes
|
||||
if not skip_profiles:
|
||||
# Trigger automation re-evaluation after all changes
|
||||
if not skip_automations:
|
||||
try:
|
||||
await profile_engine.trigger_evaluate()
|
||||
await automation_engine.trigger_evaluate()
|
||||
except Exception as e:
|
||||
errors.append(f"Profile re-evaluation: {e}")
|
||||
errors.append(f"Automation re-evaluation: {e}")
|
||||
|
||||
status = "activated" if not errors else "partial"
|
||||
if errors:
|
||||
|
||||
Reference in New Issue
Block a user