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,4 +1,4 @@
|
||||
"""Profile and Condition data models."""
|
||||
"""Automation and Condition data models."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -35,7 +35,7 @@ class Condition:
|
||||
|
||||
@dataclass
|
||||
class AlwaysCondition(Condition):
|
||||
"""Always-true condition — profile activates unconditionally when enabled."""
|
||||
"""Always-true condition — automation activates unconditionally when enabled."""
|
||||
|
||||
condition_type: str = "always"
|
||||
|
||||
@@ -159,8 +159,8 @@ class MQTTCondition(Condition):
|
||||
|
||||
|
||||
@dataclass
|
||||
class Profile:
|
||||
"""Automation profile that activates a scene preset based on conditions."""
|
||||
class Automation:
|
||||
"""Automation that activates a scene preset based on conditions."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
@@ -188,7 +188,7 @@ class Profile:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "Profile":
|
||||
def from_dict(cls, data: dict) -> "Automation":
|
||||
conditions = []
|
||||
for c_data in data.get("conditions", []):
|
||||
try:
|
||||
158
server/src/wled_controller/storage/automation_store.py
Normal file
158
server/src/wled_controller/storage/automation_store.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""Automation storage using JSON files."""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from wled_controller.storage.automation import Automation, Condition
|
||||
from wled_controller.utils import atomic_write_json, get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AutomationStore:
|
||||
"""Persistent storage for automations."""
|
||||
|
||||
def __init__(self, file_path: str):
|
||||
self.file_path = Path(file_path)
|
||||
self._automations: Dict[str, Automation] = {}
|
||||
self._load()
|
||||
|
||||
def _load(self) -> None:
|
||||
if not self.file_path.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self.file_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Support both old "profiles" key and new "automations" key
|
||||
automations_data = data.get("automations", data.get("profiles", {}))
|
||||
loaded = 0
|
||||
for auto_id, auto_dict in automations_data.items():
|
||||
try:
|
||||
automation = Automation.from_dict(auto_dict)
|
||||
self._automations[auto_id] = automation
|
||||
loaded += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load automation {auto_id}: {e}", exc_info=True)
|
||||
|
||||
if loaded > 0:
|
||||
logger.info(f"Loaded {loaded} automations from storage")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load automations from {self.file_path}: {e}")
|
||||
raise
|
||||
|
||||
logger.info(f"Automation store initialized with {len(self._automations)} automations")
|
||||
|
||||
def _save(self) -> None:
|
||||
try:
|
||||
data = {
|
||||
"version": "1.0.0",
|
||||
"automations": {
|
||||
aid: a.to_dict() for aid, a in self._automations.items()
|
||||
},
|
||||
}
|
||||
atomic_write_json(self.file_path, data)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save automations to {self.file_path}: {e}")
|
||||
raise
|
||||
|
||||
def get_all_automations(self) -> List[Automation]:
|
||||
return list(self._automations.values())
|
||||
|
||||
def get_automation(self, automation_id: str) -> Automation:
|
||||
if automation_id not in self._automations:
|
||||
raise ValueError(f"Automation not found: {automation_id}")
|
||||
return self._automations[automation_id]
|
||||
|
||||
def create_automation(
|
||||
self,
|
||||
name: str,
|
||||
enabled: bool = True,
|
||||
condition_logic: str = "or",
|
||||
conditions: Optional[List[Condition]] = None,
|
||||
scene_preset_id: Optional[str] = None,
|
||||
deactivation_mode: str = "none",
|
||||
deactivation_scene_preset_id: Optional[str] = None,
|
||||
) -> Automation:
|
||||
for a in self._automations.values():
|
||||
if a.name == name:
|
||||
raise ValueError(f"Automation with name '{name}' already exists")
|
||||
|
||||
automation_id = f"auto_{uuid.uuid4().hex[:8]}"
|
||||
now = datetime.utcnow()
|
||||
|
||||
automation = Automation(
|
||||
id=automation_id,
|
||||
name=name,
|
||||
enabled=enabled,
|
||||
condition_logic=condition_logic,
|
||||
conditions=conditions or [],
|
||||
scene_preset_id=scene_preset_id,
|
||||
deactivation_mode=deactivation_mode,
|
||||
deactivation_scene_preset_id=deactivation_scene_preset_id,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
self._automations[automation_id] = automation
|
||||
self._save()
|
||||
|
||||
logger.info(f"Created automation: {name} ({automation_id})")
|
||||
return automation
|
||||
|
||||
def update_automation(
|
||||
self,
|
||||
automation_id: str,
|
||||
name: Optional[str] = None,
|
||||
enabled: Optional[bool] = None,
|
||||
condition_logic: Optional[str] = None,
|
||||
conditions: Optional[List[Condition]] = None,
|
||||
scene_preset_id: str = "__unset__",
|
||||
deactivation_mode: Optional[str] = None,
|
||||
deactivation_scene_preset_id: str = "__unset__",
|
||||
) -> Automation:
|
||||
if automation_id not in self._automations:
|
||||
raise ValueError(f"Automation not found: {automation_id}")
|
||||
|
||||
automation = self._automations[automation_id]
|
||||
|
||||
if name is not None:
|
||||
for aid, a in self._automations.items():
|
||||
if aid != automation_id and a.name == name:
|
||||
raise ValueError(f"Automation with name '{name}' already exists")
|
||||
automation.name = name
|
||||
if enabled is not None:
|
||||
automation.enabled = enabled
|
||||
if condition_logic is not None:
|
||||
automation.condition_logic = condition_logic
|
||||
if conditions is not None:
|
||||
automation.conditions = conditions
|
||||
if scene_preset_id != "__unset__":
|
||||
automation.scene_preset_id = scene_preset_id
|
||||
if deactivation_mode is not None:
|
||||
automation.deactivation_mode = deactivation_mode
|
||||
if deactivation_scene_preset_id != "__unset__":
|
||||
automation.deactivation_scene_preset_id = deactivation_scene_preset_id
|
||||
|
||||
automation.updated_at = datetime.utcnow()
|
||||
self._save()
|
||||
|
||||
logger.info(f"Updated automation: {automation_id}")
|
||||
return automation
|
||||
|
||||
def delete_automation(self, automation_id: str) -> None:
|
||||
if automation_id not in self._automations:
|
||||
raise ValueError(f"Automation not found: {automation_id}")
|
||||
|
||||
del self._automations[automation_id]
|
||||
self._save()
|
||||
|
||||
logger.info(f"Deleted automation: {automation_id}")
|
||||
|
||||
def count(self) -> int:
|
||||
return len(self._automations)
|
||||
@@ -1,157 +0,0 @@
|
||||
"""Profile storage using JSON files."""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from wled_controller.storage.profile import Condition, Profile
|
||||
from wled_controller.utils import atomic_write_json, get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ProfileStore:
|
||||
"""Persistent storage for automation profiles."""
|
||||
|
||||
def __init__(self, file_path: str):
|
||||
self.file_path = Path(file_path)
|
||||
self._profiles: Dict[str, Profile] = {}
|
||||
self._load()
|
||||
|
||||
def _load(self) -> None:
|
||||
if not self.file_path.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self.file_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
profiles_data = data.get("profiles", {})
|
||||
loaded = 0
|
||||
for profile_id, profile_dict in profiles_data.items():
|
||||
try:
|
||||
profile = Profile.from_dict(profile_dict)
|
||||
self._profiles[profile_id] = profile
|
||||
loaded += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load profile {profile_id}: {e}", exc_info=True)
|
||||
|
||||
if loaded > 0:
|
||||
logger.info(f"Loaded {loaded} profiles from storage")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load profiles from {self.file_path}: {e}")
|
||||
raise
|
||||
|
||||
logger.info(f"Profile store initialized with {len(self._profiles)} profiles")
|
||||
|
||||
def _save(self) -> None:
|
||||
try:
|
||||
data = {
|
||||
"version": "1.0.0",
|
||||
"profiles": {
|
||||
pid: p.to_dict() for pid, p in self._profiles.items()
|
||||
},
|
||||
}
|
||||
atomic_write_json(self.file_path, data)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save profiles to {self.file_path}: {e}")
|
||||
raise
|
||||
|
||||
def get_all_profiles(self) -> List[Profile]:
|
||||
return list(self._profiles.values())
|
||||
|
||||
def get_profile(self, profile_id: str) -> Profile:
|
||||
if profile_id not in self._profiles:
|
||||
raise ValueError(f"Profile not found: {profile_id}")
|
||||
return self._profiles[profile_id]
|
||||
|
||||
def create_profile(
|
||||
self,
|
||||
name: str,
|
||||
enabled: bool = True,
|
||||
condition_logic: str = "or",
|
||||
conditions: Optional[List[Condition]] = None,
|
||||
scene_preset_id: Optional[str] = None,
|
||||
deactivation_mode: str = "none",
|
||||
deactivation_scene_preset_id: Optional[str] = None,
|
||||
) -> Profile:
|
||||
for p in self._profiles.values():
|
||||
if p.name == name:
|
||||
raise ValueError(f"Profile with name '{name}' already exists")
|
||||
|
||||
profile_id = f"prof_{uuid.uuid4().hex[:8]}"
|
||||
now = datetime.utcnow()
|
||||
|
||||
profile = Profile(
|
||||
id=profile_id,
|
||||
name=name,
|
||||
enabled=enabled,
|
||||
condition_logic=condition_logic,
|
||||
conditions=conditions or [],
|
||||
scene_preset_id=scene_preset_id,
|
||||
deactivation_mode=deactivation_mode,
|
||||
deactivation_scene_preset_id=deactivation_scene_preset_id,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
self._profiles[profile_id] = profile
|
||||
self._save()
|
||||
|
||||
logger.info(f"Created profile: {name} ({profile_id})")
|
||||
return profile
|
||||
|
||||
def update_profile(
|
||||
self,
|
||||
profile_id: str,
|
||||
name: Optional[str] = None,
|
||||
enabled: Optional[bool] = None,
|
||||
condition_logic: Optional[str] = None,
|
||||
conditions: Optional[List[Condition]] = None,
|
||||
scene_preset_id: str = "__unset__",
|
||||
deactivation_mode: Optional[str] = None,
|
||||
deactivation_scene_preset_id: str = "__unset__",
|
||||
) -> Profile:
|
||||
if profile_id not in self._profiles:
|
||||
raise ValueError(f"Profile not found: {profile_id}")
|
||||
|
||||
profile = self._profiles[profile_id]
|
||||
|
||||
if name is not None:
|
||||
for pid, p in self._profiles.items():
|
||||
if pid != profile_id and p.name == name:
|
||||
raise ValueError(f"Profile with name '{name}' already exists")
|
||||
profile.name = name
|
||||
if enabled is not None:
|
||||
profile.enabled = enabled
|
||||
if condition_logic is not None:
|
||||
profile.condition_logic = condition_logic
|
||||
if conditions is not None:
|
||||
profile.conditions = conditions
|
||||
if scene_preset_id != "__unset__":
|
||||
profile.scene_preset_id = scene_preset_id
|
||||
if deactivation_mode is not None:
|
||||
profile.deactivation_mode = deactivation_mode
|
||||
if deactivation_scene_preset_id != "__unset__":
|
||||
profile.deactivation_scene_preset_id = deactivation_scene_preset_id
|
||||
|
||||
profile.updated_at = datetime.utcnow()
|
||||
self._save()
|
||||
|
||||
logger.info(f"Updated profile: {profile_id}")
|
||||
return profile
|
||||
|
||||
def delete_profile(self, profile_id: str) -> None:
|
||||
if profile_id not in self._profiles:
|
||||
raise ValueError(f"Profile not found: {profile_id}")
|
||||
|
||||
del self._profiles[profile_id]
|
||||
self._save()
|
||||
|
||||
logger.info(f"Deleted profile: {profile_id}")
|
||||
|
||||
def count(self) -> int:
|
||||
return len(self._profiles)
|
||||
@@ -60,22 +60,22 @@ class DeviceBrightnessSnapshot:
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfileSnapshot:
|
||||
"""Snapshot of a profile's enabled state."""
|
||||
class AutomationSnapshot:
|
||||
"""Snapshot of an automation's enabled state."""
|
||||
|
||||
profile_id: str
|
||||
automation_id: str
|
||||
enabled: bool = True
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"profile_id": self.profile_id,
|
||||
"automation_id": self.automation_id,
|
||||
"enabled": self.enabled,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "ProfileSnapshot":
|
||||
def from_dict(cls, data: dict) -> "AutomationSnapshot":
|
||||
return cls(
|
||||
profile_id=data["profile_id"],
|
||||
automation_id=data.get("automation_id", data.get("profile_id", "")),
|
||||
enabled=data.get("enabled", True),
|
||||
)
|
||||
|
||||
@@ -90,7 +90,7 @@ class ScenePreset:
|
||||
color: str = "#4fc3f7" # accent color for the card
|
||||
targets: List[TargetSnapshot] = field(default_factory=list)
|
||||
devices: List[DeviceBrightnessSnapshot] = field(default_factory=list)
|
||||
profiles: List[ProfileSnapshot] = field(default_factory=list)
|
||||
automations: List[AutomationSnapshot] = field(default_factory=list)
|
||||
order: int = 0
|
||||
created_at: datetime = field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = field(default_factory=datetime.utcnow)
|
||||
@@ -103,7 +103,7 @@ class ScenePreset:
|
||||
"color": self.color,
|
||||
"targets": [t.to_dict() for t in self.targets],
|
||||
"devices": [d.to_dict() for d in self.devices],
|
||||
"profiles": [p.to_dict() for p in self.profiles],
|
||||
"automations": [a.to_dict() for a in self.automations],
|
||||
"order": self.order,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
@@ -118,7 +118,7 @@ class ScenePreset:
|
||||
color=data.get("color", "#4fc3f7"),
|
||||
targets=[TargetSnapshot.from_dict(t) for t in data.get("targets", [])],
|
||||
devices=[DeviceBrightnessSnapshot.from_dict(d) for d in data.get("devices", [])],
|
||||
profiles=[ProfileSnapshot.from_dict(p) for p in data.get("profiles", [])],
|
||||
automations=[AutomationSnapshot.from_dict(a) for a in data.get("automations", data.get("profiles", []))],
|
||||
order=data.get("order", 0),
|
||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
||||
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
|
||||
|
||||
Reference in New Issue
Block a user