From 21248e2dc9a40186a61b081b0cc2dc4130326601 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 28 Feb 2026 18:01:39 +0300 Subject: [PATCH] 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 --- server/src/wled_controller/api/__init__.py | 4 +- .../src/wled_controller/api/dependencies.py | 38 +- .../routes/{profiles.py => automations.py} | 206 +++++----- .../api/routes/scene_presets.py | 38 +- .../src/wled_controller/api/routes/system.py | 6 +- .../schemas/{profiles.py => automations.py} | 44 +-- .../api/schemas/scene_presets.py | 6 +- server/src/wled_controller/config.py | 2 +- .../core/automations/__init__.py | 1 + .../automation_engine.py} | 208 +++++----- .../platform_detector.py | 0 .../wled_controller/core/mqtt/mqtt_service.py | 10 +- .../wled_controller/core/profiles/__init__.py | 1 - .../core/scenes/scene_activator.py | 66 ++-- server/src/wled_controller/main.py | 28 +- .../css/{profiles.css => automations.css} | 16 +- .../wled_controller/static/css/dashboard.css | 2 +- server/src/wled_controller/static/js/app.js | 42 +- .../static/js/core/command-palette.js | 14 +- .../static/js/core/events-ws.js | 2 +- .../wled_controller/static/js/core/icons.js | 2 +- .../static/js/core/navigation.js | 4 +- .../wled_controller/static/js/core/state.js | 10 +- .../features/{profiles.js => automations.js} | 372 +++++++++--------- .../static/js/features/dashboard.js | 98 ++--- .../static/js/features/scene-presets.js | 8 +- .../static/js/features/tabs.js | 4 +- .../static/js/features/tutorials.js | 16 +- .../wled_controller/static/locales/en.json | 178 ++++----- .../wled_controller/static/locales/ru.json | 178 ++++----- .../wled_controller/static/locales/zh.json | 178 ++++----- .../storage/{profile.py => automation.py} | 10 +- .../storage/automation_store.py | 158 ++++++++ .../wled_controller/storage/profile_store.py | 157 -------- .../wled_controller/storage/scene_preset.py | 18 +- .../src/wled_controller/templates/index.html | 12 +- .../templates/modals/automation-editor.html | 110 ++++++ .../templates/modals/profile-editor.html | 110 ------ .../templates/modals/settings.html | 2 +- 39 files changed, 1180 insertions(+), 1179 deletions(-) rename server/src/wled_controller/api/routes/{profiles.py => automations.py} (55%) rename server/src/wled_controller/api/schemas/{profiles.py => automations.py} (75%) create mode 100644 server/src/wled_controller/core/automations/__init__.py rename server/src/wled_controller/core/{profiles/profile_engine.py => automations/automation_engine.py} (63%) rename server/src/wled_controller/core/{profiles => automations}/platform_detector.py (100%) delete mode 100644 server/src/wled_controller/core/profiles/__init__.py rename server/src/wled_controller/static/css/{profiles.css => automations.css} (95%) rename server/src/wled_controller/static/js/features/{profiles.js => automations.js} (58%) rename server/src/wled_controller/storage/{profile.py => automation.py} (95%) create mode 100644 server/src/wled_controller/storage/automation_store.py delete mode 100644 server/src/wled_controller/storage/profile_store.py create mode 100644 server/src/wled_controller/templates/modals/automation-editor.html delete mode 100644 server/src/wled_controller/templates/modals/profile-editor.html diff --git a/server/src/wled_controller/api/__init__.py b/server/src/wled_controller/api/__init__.py index ff0654e..f7a45c1 100644 --- a/server/src/wled_controller/api/__init__.py +++ b/server/src/wled_controller/api/__init__.py @@ -14,7 +14,7 @@ from .routes.audio import router as audio_router from .routes.audio_sources import router as audio_sources_router from .routes.audio_templates import router as audio_templates_router from .routes.value_sources import router as value_sources_router -from .routes.profiles import router as profiles_router +from .routes.automations import router as automations_router from .routes.scene_presets import router as scene_presets_router router = APIRouter() @@ -30,7 +30,7 @@ router.include_router(audio_sources_router) router.include_router(audio_templates_router) router.include_router(value_sources_router) router.include_router(picture_targets_router) -router.include_router(profiles_router) +router.include_router(automations_router) router.include_router(scene_presets_router) __all__ = ["router"] diff --git a/server/src/wled_controller/api/dependencies.py b/server/src/wled_controller/api/dependencies.py index ccc423e..02c84f1 100644 --- a/server/src/wled_controller/api/dependencies.py +++ b/server/src/wled_controller/api/dependencies.py @@ -11,9 +11,9 @@ from wled_controller.storage.color_strip_store import ColorStripStore from wled_controller.storage.audio_source_store import AudioSourceStore from wled_controller.storage.audio_template_store import AudioTemplateStore from wled_controller.storage.value_source_store import ValueSourceStore -from wled_controller.storage.profile_store import ProfileStore +from wled_controller.storage.automation_store import AutomationStore from wled_controller.storage.scene_preset_store import ScenePresetStore -from wled_controller.core.profiles.profile_engine import ProfileEngine +from wled_controller.core.automations.automation_engine import AutomationEngine from wled_controller.core.backup.auto_backup import AutoBackupEngine # Global instances (initialized in main.py) @@ -29,9 +29,9 @@ _audio_source_store: AudioSourceStore | None = None _audio_template_store: AudioTemplateStore | None = None _value_source_store: ValueSourceStore | None = None _processor_manager: ProcessorManager | None = None -_profile_store: ProfileStore | None = None +_automation_store: AutomationStore | None = None _scene_preset_store: ScenePresetStore | None = None -_profile_engine: ProfileEngine | None = None +_automation_engine: AutomationEngine | None = None def get_device_store() -> DeviceStore: @@ -111,11 +111,11 @@ def get_processor_manager() -> ProcessorManager: return _processor_manager -def get_profile_store() -> ProfileStore: - """Get profile store dependency.""" - if _profile_store is None: - raise RuntimeError("Profile store not initialized") - return _profile_store +def get_automation_store() -> AutomationStore: + """Get automation store dependency.""" + if _automation_store is None: + raise RuntimeError("Automation store not initialized") + return _automation_store def get_scene_preset_store() -> ScenePresetStore: @@ -125,11 +125,11 @@ def get_scene_preset_store() -> ScenePresetStore: return _scene_preset_store -def get_profile_engine() -> ProfileEngine: - """Get profile engine dependency.""" - if _profile_engine is None: - raise RuntimeError("Profile engine not initialized") - return _profile_engine +def get_automation_engine() -> AutomationEngine: + """Get automation engine dependency.""" + if _automation_engine is None: + raise RuntimeError("Automation engine not initialized") + return _automation_engine def get_auto_backup_engine() -> AutoBackupEngine: @@ -151,16 +151,16 @@ def init_dependencies( audio_source_store: AudioSourceStore | None = None, audio_template_store: AudioTemplateStore | None = None, value_source_store: ValueSourceStore | None = None, - profile_store: ProfileStore | None = None, + automation_store: AutomationStore | None = None, scene_preset_store: ScenePresetStore | None = None, - profile_engine: ProfileEngine | None = None, + automation_engine: AutomationEngine | None = None, auto_backup_engine: AutoBackupEngine | None = None, ): """Initialize global dependencies.""" global _device_store, _template_store, _processor_manager global _pp_template_store, _pattern_template_store, _picture_source_store, _picture_target_store global _color_strip_store, _audio_source_store, _audio_template_store - global _value_source_store, _profile_store, _scene_preset_store, _profile_engine, _auto_backup_engine + global _value_source_store, _automation_store, _scene_preset_store, _automation_engine, _auto_backup_engine _device_store = device_store _template_store = template_store _processor_manager = processor_manager @@ -172,7 +172,7 @@ def init_dependencies( _audio_source_store = audio_source_store _audio_template_store = audio_template_store _value_source_store = value_source_store - _profile_store = profile_store + _automation_store = automation_store _scene_preset_store = scene_preset_store - _profile_engine = profile_engine + _automation_engine = automation_engine _auto_backup_engine = auto_backup_engine diff --git a/server/src/wled_controller/api/routes/profiles.py b/server/src/wled_controller/api/routes/automations.py similarity index 55% rename from server/src/wled_controller/api/routes/profiles.py rename to server/src/wled_controller/api/routes/automations.py index 9a6dcbb..e1f4c06 100644 --- a/server/src/wled_controller/api/routes/profiles.py +++ b/server/src/wled_controller/api/routes/automations.py @@ -1,22 +1,22 @@ -"""Profile management API routes.""" +"""Automation management API routes.""" from fastapi import APIRouter, Depends, HTTPException from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( - get_profile_engine, - get_profile_store, + get_automation_engine, + get_automation_store, get_scene_preset_store, ) -from wled_controller.api.schemas.profiles import ( +from wled_controller.api.schemas.automations import ( + AutomationCreate, + AutomationListResponse, + AutomationResponse, + AutomationUpdate, ConditionSchema, - ProfileCreate, - ProfileListResponse, - ProfileResponse, - ProfileUpdate, ) -from wled_controller.core.profiles.profile_engine import ProfileEngine -from wled_controller.storage.profile import ( +from wled_controller.core.automations.automation_engine import AutomationEngine +from wled_controller.storage.automation import ( AlwaysCondition, ApplicationCondition, Condition, @@ -25,7 +25,7 @@ from wled_controller.storage.profile import ( SystemIdleCondition, TimeOfDayCondition, ) -from wled_controller.storage.profile_store import ProfileStore +from wled_controller.storage.automation_store import AutomationStore from wled_controller.storage.scene_preset_store import ScenePresetStore from wled_controller.utils import get_logger @@ -71,22 +71,22 @@ def _condition_to_schema(c: Condition) -> ConditionSchema: return ConditionSchema(**d) -def _profile_to_response(profile, engine: ProfileEngine) -> ProfileResponse: - state = engine.get_profile_state(profile.id) - return ProfileResponse( - id=profile.id, - name=profile.name, - enabled=profile.enabled, - condition_logic=profile.condition_logic, - conditions=[_condition_to_schema(c) for c in profile.conditions], - scene_preset_id=profile.scene_preset_id, - deactivation_mode=profile.deactivation_mode, - deactivation_scene_preset_id=profile.deactivation_scene_preset_id, +def _automation_to_response(automation, engine: AutomationEngine) -> AutomationResponse: + state = engine.get_automation_state(automation.id) + return AutomationResponse( + id=automation.id, + name=automation.name, + enabled=automation.enabled, + condition_logic=automation.condition_logic, + conditions=[_condition_to_schema(c) for c in automation.conditions], + scene_preset_id=automation.scene_preset_id, + deactivation_mode=automation.deactivation_mode, + deactivation_scene_preset_id=automation.deactivation_scene_preset_id, is_active=state["is_active"], last_activated_at=state.get("last_activated_at"), last_deactivated_at=state.get("last_deactivated_at"), - created_at=profile.created_at, - updated_at=profile.updated_at, + created_at=automation.created_at, + updated_at=automation.updated_at, ) @@ -115,19 +115,19 @@ def _validate_scene_refs( # ===== CRUD Endpoints ===== @router.post( - "/api/v1/profiles", - response_model=ProfileResponse, - tags=["Profiles"], + "/api/v1/automations", + response_model=AutomationResponse, + tags=["Automations"], status_code=201, ) -async def create_profile( - data: ProfileCreate, +async def create_automation( + data: AutomationCreate, _auth: AuthRequired, - store: ProfileStore = Depends(get_profile_store), - engine: ProfileEngine = Depends(get_profile_engine), + store: AutomationStore = Depends(get_automation_store), + engine: AutomationEngine = Depends(get_automation_engine), scene_store: ScenePresetStore = Depends(get_scene_preset_store), ): - """Create a new profile.""" + """Create a new automation.""" _validate_condition_logic(data.condition_logic) _validate_scene_refs(data.scene_preset_id, data.deactivation_scene_preset_id, scene_store) @@ -136,7 +136,7 @@ async def create_profile( except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) - profile = store.create_profile( + automation = store.create_automation( name=data.name, enabled=data.enabled, condition_logic=data.condition_logic, @@ -146,64 +146,64 @@ async def create_profile( deactivation_scene_preset_id=data.deactivation_scene_preset_id, ) - if profile.enabled: + if automation.enabled: await engine.trigger_evaluate() - return _profile_to_response(profile, engine) + return _automation_to_response(automation, engine) @router.get( - "/api/v1/profiles", - response_model=ProfileListResponse, - tags=["Profiles"], + "/api/v1/automations", + response_model=AutomationListResponse, + tags=["Automations"], ) -async def list_profiles( +async def list_automations( _auth: AuthRequired, - store: ProfileStore = Depends(get_profile_store), - engine: ProfileEngine = Depends(get_profile_engine), + store: AutomationStore = Depends(get_automation_store), + engine: AutomationEngine = Depends(get_automation_engine), ): - """List all profiles.""" - profiles = store.get_all_profiles() - return ProfileListResponse( - profiles=[_profile_to_response(p, engine) for p in profiles], - count=len(profiles), + """List all automations.""" + automations = store.get_all_automations() + return AutomationListResponse( + automations=[_automation_to_response(a, engine) for a in automations], + count=len(automations), ) @router.get( - "/api/v1/profiles/{profile_id}", - response_model=ProfileResponse, - tags=["Profiles"], + "/api/v1/automations/{automation_id}", + response_model=AutomationResponse, + tags=["Automations"], ) -async def get_profile( - profile_id: str, +async def get_automation( + automation_id: str, _auth: AuthRequired, - store: ProfileStore = Depends(get_profile_store), - engine: ProfileEngine = Depends(get_profile_engine), + store: AutomationStore = Depends(get_automation_store), + engine: AutomationEngine = Depends(get_automation_engine), ): - """Get a single profile.""" + """Get a single automation.""" try: - profile = store.get_profile(profile_id) + automation = store.get_automation(automation_id) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) - return _profile_to_response(profile, engine) + return _automation_to_response(automation, engine) @router.put( - "/api/v1/profiles/{profile_id}", - response_model=ProfileResponse, - tags=["Profiles"], + "/api/v1/automations/{automation_id}", + response_model=AutomationResponse, + tags=["Automations"], ) -async def update_profile( - profile_id: str, - data: ProfileUpdate, +async def update_automation( + automation_id: str, + data: AutomationUpdate, _auth: AuthRequired, - store: ProfileStore = Depends(get_profile_store), - engine: ProfileEngine = Depends(get_profile_engine), + store: AutomationStore = Depends(get_automation_store), + engine: AutomationEngine = Depends(get_automation_engine), scene_store: ScenePresetStore = Depends(get_scene_preset_store), ): - """Update a profile.""" + """Update an automation.""" if data.condition_logic is not None: _validate_condition_logic(data.condition_logic) @@ -220,11 +220,11 @@ async def update_profile( try: # If disabling, deactivate first if data.enabled is False: - await engine.deactivate_if_active(profile_id) + await engine.deactivate_if_active(automation_id) # Build update kwargs — use sentinel for Optional[str] fields update_kwargs = dict( - profile_id=profile_id, + automation_id=automation_id, name=data.name, enabled=data.enabled, condition_logic=data.condition_logic, @@ -236,34 +236,34 @@ async def update_profile( if data.deactivation_scene_preset_id is not None: update_kwargs["deactivation_scene_preset_id"] = data.deactivation_scene_preset_id - profile = store.update_profile(**update_kwargs) + automation = store.update_automation(**update_kwargs) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) - # Re-evaluate immediately if profile is enabled (may have new conditions/scene) - if profile.enabled: + # Re-evaluate immediately if automation is enabled (may have new conditions/scene) + if automation.enabled: await engine.trigger_evaluate() - return _profile_to_response(profile, engine) + return _automation_to_response(automation, engine) @router.delete( - "/api/v1/profiles/{profile_id}", + "/api/v1/automations/{automation_id}", status_code=204, - tags=["Profiles"], + tags=["Automations"], ) -async def delete_profile( - profile_id: str, +async def delete_automation( + automation_id: str, _auth: AuthRequired, - store: ProfileStore = Depends(get_profile_store), - engine: ProfileEngine = Depends(get_profile_engine), + store: AutomationStore = Depends(get_automation_store), + engine: AutomationEngine = Depends(get_automation_engine), ): - """Delete a profile.""" - # Deactivate first (stop owned targets) - await engine.deactivate_if_active(profile_id) + """Delete an automation.""" + # Deactivate first + await engine.deactivate_if_active(automation_id) try: - store.delete_profile(profile_id) + store.delete_automation(automation_id) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @@ -271,45 +271,45 @@ async def delete_profile( # ===== Enable/Disable ===== @router.post( - "/api/v1/profiles/{profile_id}/enable", - response_model=ProfileResponse, - tags=["Profiles"], + "/api/v1/automations/{automation_id}/enable", + response_model=AutomationResponse, + tags=["Automations"], ) -async def enable_profile( - profile_id: str, +async def enable_automation( + automation_id: str, _auth: AuthRequired, - store: ProfileStore = Depends(get_profile_store), - engine: ProfileEngine = Depends(get_profile_engine), + store: AutomationStore = Depends(get_automation_store), + engine: AutomationEngine = Depends(get_automation_engine), ): - """Enable a profile.""" + """Enable an automation.""" try: - profile = store.update_profile(profile_id=profile_id, enabled=True) + automation = store.update_automation(automation_id=automation_id, enabled=True) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) - # Evaluate immediately so targets start without waiting for the next poll cycle + # Evaluate immediately so scene activates without waiting for the next poll cycle await engine.trigger_evaluate() - return _profile_to_response(profile, engine) + return _automation_to_response(automation, engine) @router.post( - "/api/v1/profiles/{profile_id}/disable", - response_model=ProfileResponse, - tags=["Profiles"], + "/api/v1/automations/{automation_id}/disable", + response_model=AutomationResponse, + tags=["Automations"], ) -async def disable_profile( - profile_id: str, +async def disable_automation( + automation_id: str, _auth: AuthRequired, - store: ProfileStore = Depends(get_profile_store), - engine: ProfileEngine = Depends(get_profile_engine), + store: AutomationStore = Depends(get_automation_store), + engine: AutomationEngine = Depends(get_automation_engine), ): - """Disable a profile and stop any targets it owns.""" - await engine.deactivate_if_active(profile_id) + """Disable an automation and deactivate it.""" + await engine.deactivate_if_active(automation_id) try: - profile = store.update_profile(profile_id=profile_id, enabled=False) + automation = store.update_automation(automation_id=automation_id, enabled=False) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) - return _profile_to_response(profile, engine) + return _automation_to_response(automation, engine) diff --git a/server/src/wled_controller/api/routes/scene_presets.py b/server/src/wled_controller/api/routes/scene_presets.py index 69234b9..42849cb 100644 --- a/server/src/wled_controller/api/routes/scene_presets.py +++ b/server/src/wled_controller/api/routes/scene_presets.py @@ -7,11 +7,11 @@ from fastapi import APIRouter, Depends, HTTPException from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( + get_automation_engine, + get_automation_store, get_device_store, get_picture_target_store, get_processor_manager, - get_profile_engine, - get_profile_store, get_scene_preset_store, ) from wled_controller.api.schemas.scene_presets import ( @@ -28,10 +28,10 @@ from wled_controller.core.scenes.scene_activator import ( ) 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 ScenePreset from wled_controller.storage.scene_preset_store import ScenePresetStore -from wled_controller.core.profiles.profile_engine import ProfileEngine +from wled_controller.core.automations.automation_engine import AutomationEngine from wled_controller.utils import get_logger logger = get_logger(__name__) @@ -56,10 +56,10 @@ def _preset_to_response(preset: ScenePreset) -> ScenePresetResponse: "device_id": d.device_id, "software_brightness": d.software_brightness, } for d in preset.devices], - profiles=[{ - "profile_id": p.profile_id, - "enabled": p.enabled, - } for p in preset.profiles], + automations=[{ + "automation_id": a.automation_id, + "enabled": a.enabled, + } for a in preset.automations], order=preset.order, created_at=preset.created_at, updated_at=preset.updated_at, @@ -80,12 +80,12 @@ async def create_scene_preset( store: ScenePresetStore = Depends(get_scene_preset_store), target_store: PictureTargetStore = Depends(get_picture_target_store), device_store: DeviceStore = Depends(get_device_store), - profile_store: ProfileStore = Depends(get_profile_store), + automation_store: AutomationStore = Depends(get_automation_store), manager: ProcessorManager = Depends(get_processor_manager), ): """Capture current state as a new scene preset.""" - targets, devices, profiles = capture_current_snapshot( - target_store, device_store, profile_store, manager, + targets, devices, automations = capture_current_snapshot( + target_store, device_store, automation_store, manager, ) now = datetime.utcnow() @@ -96,7 +96,7 @@ async def create_scene_preset( color=data.color, targets=targets, devices=devices, - profiles=profiles, + automations=automations, order=store.count(), created_at=now, updated_at=now, @@ -200,12 +200,12 @@ async def recapture_scene_preset( store: ScenePresetStore = Depends(get_scene_preset_store), target_store: PictureTargetStore = Depends(get_picture_target_store), device_store: DeviceStore = Depends(get_device_store), - profile_store: ProfileStore = Depends(get_profile_store), + automation_store: AutomationStore = Depends(get_automation_store), manager: ProcessorManager = Depends(get_processor_manager), ): """Re-capture current state into an existing preset (updates snapshot).""" - targets, devices, profiles = capture_current_snapshot( - target_store, device_store, profile_store, manager, + targets, devices, automations = capture_current_snapshot( + target_store, device_store, automation_store, manager, ) new_snapshot = ScenePreset( @@ -213,7 +213,7 @@ async def recapture_scene_preset( name="", targets=targets, devices=devices, - profiles=profiles, + automations=automations, ) try: @@ -237,8 +237,8 @@ async def activate_scene_preset( store: ScenePresetStore = Depends(get_scene_preset_store), target_store: PictureTargetStore = Depends(get_picture_target_store), device_store: DeviceStore = Depends(get_device_store), - profile_store: ProfileStore = Depends(get_profile_store), - engine: ProfileEngine = Depends(get_profile_engine), + automation_store: AutomationStore = Depends(get_automation_store), + engine: AutomationEngine = Depends(get_automation_engine), manager: ProcessorManager = Depends(get_processor_manager), ): """Activate a scene preset — restore the captured state.""" @@ -248,7 +248,7 @@ async def activate_scene_preset( raise HTTPException(status_code=404, detail=str(e)) status, errors = await apply_scene_state( - preset, target_store, device_store, profile_store, engine, manager, + preset, target_store, device_store, automation_store, engine, manager, ) if not errors: diff --git a/server/src/wled_controller/api/routes/system.py b/server/src/wled_controller/api/routes/system.py index ad8b99a..f8e4def 100644 --- a/server/src/wled_controller/api/routes/system.py +++ b/server/src/wled_controller/api/routes/system.py @@ -182,9 +182,9 @@ async def get_displays( async def get_running_processes(_: AuthRequired): """Get list of currently running process names. - Returns a sorted list of unique process names for use in profile conditions. + Returns a sorted list of unique process names for use in automation conditions. """ - from wled_controller.core.profiles.platform_detector import PlatformDetector + from wled_controller.core.automations.platform_detector import PlatformDetector try: detector = PlatformDetector() @@ -271,7 +271,7 @@ STORE_MAP = { "audio_sources": "audio_sources_file", "audio_templates": "audio_templates_file", "value_sources": "value_sources_file", - "profiles": "profiles_file", + "automations": "automations_file", "scene_presets": "scene_presets_file", } diff --git a/server/src/wled_controller/api/schemas/profiles.py b/server/src/wled_controller/api/schemas/automations.py similarity index 75% rename from server/src/wled_controller/api/schemas/profiles.py rename to server/src/wled_controller/api/schemas/automations.py index fd8193c..38fdd8c 100644 --- a/server/src/wled_controller/api/schemas/profiles.py +++ b/server/src/wled_controller/api/schemas/automations.py @@ -1,4 +1,4 @@ -"""Profile-related schemas.""" +"""Automation-related schemas.""" from datetime import datetime from typing import List, Optional @@ -7,7 +7,7 @@ from pydantic import BaseModel, Field class ConditionSchema(BaseModel): - """A single condition within a profile.""" + """A single condition within an automation.""" condition_type: str = Field(description="Condition type discriminator (e.g. 'application')") # Application condition fields @@ -27,11 +27,11 @@ class ConditionSchema(BaseModel): match_mode: Optional[str] = Field(None, description="'exact', 'contains', or 'regex' (for mqtt condition)") -class ProfileCreate(BaseModel): - """Request to create a profile.""" +class AutomationCreate(BaseModel): + """Request to create an automation.""" - name: str = Field(description="Profile name", min_length=1, max_length=100) - enabled: bool = Field(default=True, description="Whether the profile is enabled") + name: str = Field(description="Automation name", min_length=1, max_length=100) + enabled: bool = Field(default=True, description="Whether the automation is enabled") condition_logic: str = Field(default="or", description="How conditions combine: 'or' or 'and'") conditions: List[ConditionSchema] = Field(default_factory=list, description="List of conditions") scene_preset_id: Optional[str] = Field(None, description="Scene preset to activate") @@ -39,11 +39,11 @@ class ProfileCreate(BaseModel): deactivation_scene_preset_id: Optional[str] = Field(None, description="Scene preset for fallback deactivation") -class ProfileUpdate(BaseModel): - """Request to update a profile.""" +class AutomationUpdate(BaseModel): + """Request to update an automation.""" - name: Optional[str] = Field(None, description="Profile name", min_length=1, max_length=100) - enabled: Optional[bool] = Field(None, description="Whether the profile is enabled") + name: Optional[str] = Field(None, description="Automation name", min_length=1, max_length=100) + enabled: Optional[bool] = Field(None, description="Whether the automation is enabled") condition_logic: Optional[str] = Field(None, description="How conditions combine: 'or' or 'and'") conditions: Optional[List[ConditionSchema]] = Field(None, description="List of conditions") scene_preset_id: Optional[str] = Field(None, description="Scene preset to activate") @@ -51,26 +51,26 @@ class ProfileUpdate(BaseModel): deactivation_scene_preset_id: Optional[str] = Field(None, description="Scene preset for fallback deactivation") -class ProfileResponse(BaseModel): - """Profile information response.""" +class AutomationResponse(BaseModel): + """Automation information response.""" - id: str = Field(description="Profile ID") - name: str = Field(description="Profile name") - enabled: bool = Field(description="Whether the profile is enabled") + id: str = Field(description="Automation ID") + name: str = Field(description="Automation name") + enabled: bool = Field(description="Whether the automation is enabled") condition_logic: str = Field(description="Condition combination logic") conditions: List[ConditionSchema] = Field(description="List of conditions") scene_preset_id: Optional[str] = Field(None, description="Scene preset to activate") deactivation_mode: str = Field(default="none", description="Deactivation behavior") deactivation_scene_preset_id: Optional[str] = Field(None, description="Fallback scene preset") - is_active: bool = Field(default=False, description="Whether the profile is currently active") - last_activated_at: Optional[datetime] = Field(None, description="Last time this profile was activated") - last_deactivated_at: Optional[datetime] = Field(None, description="Last time this profile was deactivated") + is_active: bool = Field(default=False, description="Whether the automation is currently active") + last_activated_at: Optional[datetime] = Field(None, description="Last time this automation was activated") + last_deactivated_at: Optional[datetime] = Field(None, description="Last time this automation was deactivated") created_at: datetime = Field(description="Creation timestamp") updated_at: datetime = Field(description="Last update timestamp") -class ProfileListResponse(BaseModel): - """List of profiles response.""" +class AutomationListResponse(BaseModel): + """List of automations response.""" - profiles: List[ProfileResponse] = Field(description="List of profiles") - count: int = Field(description="Number of profiles") + automations: List[AutomationResponse] = Field(description="List of automations") + count: int = Field(description="Number of automations") diff --git a/server/src/wled_controller/api/schemas/scene_presets.py b/server/src/wled_controller/api/schemas/scene_presets.py index d198b01..06fdcc1 100644 --- a/server/src/wled_controller/api/schemas/scene_presets.py +++ b/server/src/wled_controller/api/schemas/scene_presets.py @@ -20,8 +20,8 @@ class DeviceBrightnessSnapshotSchema(BaseModel): software_brightness: int = 255 -class ProfileSnapshotSchema(BaseModel): - profile_id: str +class AutomationSnapshotSchema(BaseModel): + automation_id: str enabled: bool = True @@ -51,7 +51,7 @@ class ScenePresetResponse(BaseModel): color: str targets: List[TargetSnapshotSchema] devices: List[DeviceBrightnessSnapshotSchema] - profiles: List[ProfileSnapshotSchema] + automations: List[AutomationSnapshotSchema] order: int created_at: datetime updated_at: datetime diff --git a/server/src/wled_controller/config.py b/server/src/wled_controller/config.py index 308c66a..c67bd7a 100644 --- a/server/src/wled_controller/config.py +++ b/server/src/wled_controller/config.py @@ -37,7 +37,7 @@ class StorageConfig(BaseSettings): audio_sources_file: str = "data/audio_sources.json" audio_templates_file: str = "data/audio_templates.json" value_sources_file: str = "data/value_sources.json" - profiles_file: str = "data/profiles.json" + automations_file: str = "data/automations.json" scene_presets_file: str = "data/scene_presets.json" diff --git a/server/src/wled_controller/core/automations/__init__.py b/server/src/wled_controller/core/automations/__init__.py new file mode 100644 index 0000000..d22aeeb --- /dev/null +++ b/server/src/wled_controller/core/automations/__init__.py @@ -0,0 +1 @@ +"""Automation engine — condition evaluation and scene activation.""" diff --git a/server/src/wled_controller/core/profiles/profile_engine.py b/server/src/wled_controller/core/automations/automation_engine.py similarity index 63% rename from server/src/wled_controller/core/profiles/profile_engine.py rename to server/src/wled_controller/core/automations/automation_engine.py index 58ca0bc..1c2ceb9 100644 --- a/server/src/wled_controller/core/profiles/profile_engine.py +++ b/server/src/wled_controller/core/automations/automation_engine.py @@ -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) diff --git a/server/src/wled_controller/core/profiles/platform_detector.py b/server/src/wled_controller/core/automations/platform_detector.py similarity index 100% rename from server/src/wled_controller/core/profiles/platform_detector.py rename to server/src/wled_controller/core/automations/platform_detector.py diff --git a/server/src/wled_controller/core/mqtt/mqtt_service.py b/server/src/wled_controller/core/mqtt/mqtt_service.py index 2d0455c..a1385b7 100644 --- a/server/src/wled_controller/core/mqtt/mqtt_service.py +++ b/server/src/wled_controller/core/mqtt/mqtt_service.py @@ -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) diff --git a/server/src/wled_controller/core/profiles/__init__.py b/server/src/wled_controller/core/profiles/__init__.py deleted file mode 100644 index d255248..0000000 --- a/server/src/wled_controller/core/profiles/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Profile automation — condition evaluation and target management.""" diff --git a/server/src/wled_controller/core/scenes/scene_activator.py b/server/src/wled_controller/core/scenes/scene_activator.py index df79dbb..39fa2bd 100644 --- a/server/src/wled_controller/core/scenes/scene_activator.py +++ b/server/src/wled_controller/core/scenes/scene_activator.py @@ -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: diff --git a/server/src/wled_controller/main.py b/server/src/wled_controller/main.py index 346ef8b..7cf88ca 100644 --- a/server/src/wled_controller/main.py +++ b/server/src/wled_controller/main.py @@ -27,9 +27,9 @@ from wled_controller.storage.audio_source_store import AudioSourceStore from wled_controller.storage.audio_template_store import AudioTemplateStore import wled_controller.core.audio # noqa: F401 — trigger engine auto-registration from wled_controller.storage.value_source_store import ValueSourceStore -from wled_controller.storage.profile_store import ProfileStore +from wled_controller.storage.automation_store import AutomationStore from wled_controller.storage.scene_preset_store import ScenePresetStore -from wled_controller.core.profiles.profile_engine import ProfileEngine +from wled_controller.core.automations.automation_engine import AutomationEngine from wled_controller.core.mqtt.mqtt_service import MQTTService from wled_controller.core.devices.mqtt_client import set_mqtt_service from wled_controller.core.backup.auto_backup import AutoBackupEngine @@ -54,7 +54,7 @@ color_strip_store = ColorStripStore(config.storage.color_strip_sources_file) audio_source_store = AudioSourceStore(config.storage.audio_sources_file) audio_template_store = AudioTemplateStore(config.storage.audio_templates_file) value_source_store = ValueSourceStore(config.storage.value_sources_file) -profile_store = ProfileStore(config.storage.profiles_file) +automation_store = AutomationStore(config.storage.automations_file) scene_preset_store = ScenePresetStore(config.storage.scene_presets_file) # Migrate embedded audio config from CSS entities to audio sources @@ -108,9 +108,9 @@ async def lifespan(app: FastAPI): mqtt_service = MQTTService(config.mqtt) set_mqtt_service(mqtt_service) - # Create profile engine (needs processor_manager + mqtt_service + stores for scene activation) - profile_engine = ProfileEngine( - profile_store, processor_manager, + # Create automation engine (needs processor_manager + mqtt_service + stores for scene activation) + automation_engine = AutomationEngine( + automation_store, processor_manager, mqtt_service=mqtt_service, scene_preset_store=scene_preset_store, target_store=picture_target_store, @@ -136,9 +136,9 @@ async def lifespan(app: FastAPI): audio_source_store=audio_source_store, audio_template_store=audio_template_store, value_source_store=value_source_store, - profile_store=profile_store, + automation_store=automation_store, scene_preset_store=scene_preset_store, - profile_engine=profile_engine, + automation_engine=automation_engine, auto_backup_engine=auto_backup_engine, ) @@ -180,8 +180,8 @@ async def lifespan(app: FastAPI): # Start MQTT service (broker connection for output, triggers, state) await mqtt_service.start() - # Start profile engine (evaluates conditions and auto-starts/stops targets) - await profile_engine.start() + # Start automation engine (evaluates conditions and activates scenes) + await automation_engine.start() # Start auto-backup engine (periodic configuration backups) await auto_backup_engine.start() @@ -210,12 +210,12 @@ async def lifespan(app: FastAPI): except Exception as e: logger.error(f"Error stopping auto-backup engine: {e}") - # Stop profile engine first (deactivates profile-managed targets) + # Stop automation engine first (deactivates automation-managed scenes) try: - await profile_engine.stop() - logger.info("Stopped profile engine") + await automation_engine.stop() + logger.info("Stopped automation engine") except Exception as e: - logger.error(f"Error stopping profile engine: {e}") + logger.error(f"Error stopping automation engine: {e}") # Stop all processing try: diff --git a/server/src/wled_controller/static/css/profiles.css b/server/src/wled_controller/static/css/automations.css similarity index 95% rename from server/src/wled_controller/static/css/profiles.css rename to server/src/wled_controller/static/css/automations.css index 9147739..b2395b8 100644 --- a/server/src/wled_controller/static/css/profiles.css +++ b/server/src/wled_controller/static/css/automations.css @@ -1,34 +1,34 @@ -/* ===== PROFILES ===== */ +/* ===== AUTOMATIONS ===== */ -.badge-profile-active { +.badge-automation-active { background: var(--success-color); color: #fff; } -.badge-profile-inactive { +.badge-automation-inactive { background: var(--border-color); color: var(--text-color); } -.badge-profile-disabled { +.badge-automation-disabled { background: var(--border-color); color: var(--text-muted); opacity: 0.7; } -.profile-status-disabled { +.automation-status-disabled { opacity: 0.6; } -.profile-logic-label { +.automation-logic-label { font-size: 0.7rem; font-weight: 600; color: var(--text-muted); padding: 0 4px; } -/* Profile condition editor rows */ -.profile-condition-row { +/* Automation condition editor rows */ +.automation-condition-row { border: 1px solid var(--border-color); border-radius: 6px; padding: 10px; diff --git a/server/src/wled_controller/static/css/dashboard.css b/server/src/wled_controller/static/css/dashboard.css index 2d6085f..a9b6794 100644 --- a/server/src/wled_controller/static/css/dashboard.css +++ b/server/src/wled_controller/static/css/dashboard.css @@ -294,7 +294,7 @@ flex-shrink: 0; } -.dashboard-profile .dashboard-target-metrics { +.dashboard-automation .dashboard-target-metrics { min-width: 48px; } diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index 725d583..16c3891 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -23,11 +23,11 @@ import { } from './features/displays.js'; import { startCalibrationTutorial, startDeviceTutorial, startGettingStartedTutorial, - startDashboardTutorial, startTargetsTutorial, startSourcesTutorial, startProfilesTutorial, + startDashboardTutorial, startTargetsTutorial, startSourcesTutorial, startAutomationsTutorial, closeTutorial, tutorialNext, tutorialPrev, } from './features/tutorials.js'; -// Layer 4: devices, dashboard, streams, kc-targets, pattern-templates, profiles +// Layer 4: devices, dashboard, streams, kc-targets, pattern-templates, automations import { showSettings, closeDeviceSettingsModal, forceCloseDeviceSettingsModal, saveDeviceSettings, updateBrightnessLabel, saveCardBrightness, @@ -36,7 +36,7 @@ import { } from './features/devices.js'; import { loadDashboard, stopUptimeTimer, - dashboardToggleProfile, dashboardStartTarget, dashboardStopTarget, dashboardToggleAutoStart, dashboardStopAll, + dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardToggleAutoStart, dashboardStopAll, toggleDashboardSection, changeDashboardPollInterval, } from './features/dashboard.js'; import { startEventsWS, stopEventsWS } from './core/events-ws.js'; @@ -77,11 +77,11 @@ import { clonePatternTemplate, } from './features/pattern-templates.js'; import { - loadProfiles, openProfileEditor, closeProfileEditorModal, - saveProfileEditor, addProfileCondition, - toggleProfileEnabled, deleteProfile, - expandAllProfileSections, collapseAllProfileSections, -} from './features/profiles.js'; + loadAutomations, openAutomationEditor, closeAutomationEditorModal, + saveAutomationEditor, addAutomationCondition, + toggleAutomationEnabled, deleteAutomation, + expandAllAutomationSections, collapseAllAutomationSections, +} from './features/automations.js'; import { loadScenes, expandAllSceneSections, collapseAllSceneSections, openScenePresetCapture, editScenePreset, saveScenePreset, closeScenePresetEditor, @@ -185,7 +185,7 @@ Object.assign(window, { startDashboardTutorial, startTargetsTutorial, startSourcesTutorial, - startProfilesTutorial, + startAutomationsTutorial, closeTutorial, tutorialNext, tutorialPrev, @@ -204,7 +204,7 @@ Object.assign(window, { // dashboard loadDashboard, - dashboardToggleProfile, + dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardToggleAutoStart, @@ -300,16 +300,16 @@ Object.assign(window, { capturePatternBackground, clonePatternTemplate, - // profiles - loadProfiles, - openProfileEditor, - closeProfileEditorModal, - saveProfileEditor, - addProfileCondition, - toggleProfileEnabled, - deleteProfile, - expandAllProfileSections, - collapseAllProfileSections, + // automations + loadAutomations, + openAutomationEditor, + closeAutomationEditorModal, + saveAutomationEditor, + addAutomationCondition, + toggleAutomationEnabled, + deleteAutomation, + expandAllAutomationSections, + collapseAllAutomationSections, // scene presets loadScenes, @@ -440,7 +440,7 @@ document.addEventListener('keydown', (e) => { // Tab shortcuts: Ctrl+1..5 (skip when typing in inputs) if (!inInput && e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { - const tabMap = { '1': 'dashboard', '2': 'profiles', '3': 'targets', '4': 'streams', '5': 'scenes' }; + const tabMap = { '1': 'dashboard', '2': 'automations', '3': 'targets', '4': 'streams', '5': 'scenes' }; const tab = tabMap[e.key]; if (tab) { e.preventDefault(); diff --git a/server/src/wled_controller/static/js/core/command-palette.js b/server/src/wled_controller/static/js/core/command-palette.js index bbd5d7b..c706263 100644 --- a/server/src/wled_controller/static/js/core/command-palette.js +++ b/server/src/wled_controller/static/js/core/command-palette.js @@ -7,7 +7,7 @@ import { t } from './i18n.js'; import { navigateToCard } from './navigation.js'; import { getTargetTypeIcon, getPictureSourceIcon, getColorStripIcon, getAudioSourceIcon, - ICON_DEVICE, ICON_TARGET, ICON_PROFILE, ICON_VALUE_SOURCE, ICON_SCENE, + ICON_DEVICE, ICON_TARGET, ICON_AUTOMATION, ICON_VALUE_SOURCE, ICON_SCENE, ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_PATTERN_TEMPLATE, } from './icons.js'; @@ -31,7 +31,7 @@ function _mapEntities(data, mapFn) { } function _buildItems(results) { - const [devices, targets, css, profiles, capTempl, ppTempl, patTempl, audioSrc, valSrc, streams, scenePresets] = results; + const [devices, targets, css, automations, capTempl, ppTempl, patTempl, audioSrc, valSrc, streams, scenePresets] = results; const items = []; _mapEntities(devices, d => items.push({ @@ -58,9 +58,9 @@ function _buildItems(results) { nav: ['targets', 'led', 'led-css', 'data-css-id', c.id], })); - _mapEntities(profiles, p => items.push({ - name: p.name, detail: p.enabled ? 'enabled' : '', group: 'profiles', icon: ICON_PROFILE, - nav: ['profiles', null, 'profiles', 'data-profile-id', p.id], + _mapEntities(automations, a => items.push({ + name: a.name, detail: a.enabled ? 'enabled' : '', group: 'automations', icon: ICON_AUTOMATION, + nav: ['automations', null, 'automations', 'data-automation-id', a.id], })); _mapEntities(capTempl, ct => items.push({ @@ -112,7 +112,7 @@ const _responseKeys = [ ['/devices', 'devices'], ['/picture-targets', 'targets'], ['/color-strip-sources', 'sources'], - ['/profiles', 'profiles'], + ['/automations', 'automations'], ['/capture-templates', 'templates'], ['/postprocessing-templates','templates'], ['/pattern-templates', 'templates'], @@ -136,7 +136,7 @@ async function _fetchAllEntities() { // ─── Group ordering ─── const _groupOrder = [ - 'devices', 'targets', 'kc_targets', 'css', 'profiles', + 'devices', 'targets', 'kc_targets', 'css', 'automations', 'streams', 'capture_templates', 'pp_templates', 'pattern_templates', 'audio', 'value', 'scenes', ]; diff --git a/server/src/wled_controller/static/js/core/events-ws.js b/server/src/wled_controller/static/js/core/events-ws.js index 397e1cf..3c0dcaf 100644 --- a/server/src/wled_controller/static/js/core/events-ws.js +++ b/server/src/wled_controller/static/js/core/events-ws.js @@ -2,7 +2,7 @@ * Global events WebSocket — stays connected while logged in, * dispatches DOM custom events that feature modules can listen to. * - * Events dispatched: server:state_change, server:profile_state_changed + * Events dispatched: server:state_change, server:automation_state_changed */ import { apiKey } from './state.js'; diff --git a/server/src/wled_controller/static/js/core/icons.js b/server/src/wled_controller/static/js/core/icons.js index 8efa79b..4c2d673 100644 --- a/server/src/wled_controller/static/js/core/icons.js +++ b/server/src/wled_controller/static/js/core/icons.js @@ -64,7 +64,7 @@ export function getEngineIcon(engineType) { // ── Entity-kind constants ─────────────────────────────────── -export const ICON_PROFILE = _svg(P.clipboardList); +export const ICON_AUTOMATION = _svg(P.clipboardList); export const ICON_DEVICE = _svg(P.monitor); export const ICON_TARGET = _svg(P.zap); export const ICON_VALUE_SOURCE = _svg(P.hash); diff --git a/server/src/wled_controller/static/js/core/navigation.js b/server/src/wled_controller/static/js/core/navigation.js index cb02412..1ddadf0 100644 --- a/server/src/wled_controller/static/js/core/navigation.js +++ b/server/src/wled_controller/static/js/core/navigation.js @@ -7,7 +7,7 @@ import { switchTab } from '../features/tabs.js'; /** * Navigate to a card on any tab/subtab, expanding the section and scrolling to it. * - * @param {string} tab Main tab: 'dashboard' | 'profiles' | 'targets' | 'streams' + * @param {string} tab Main tab: 'dashboard' | 'automations' | 'targets' | 'streams' * @param {string|null} subTab Sub-tab key or null * @param {string|null} sectionKey CardSection key to expand, or null * @param {string} cardAttr Data attribute to find the card (e.g. 'data-device-id') @@ -87,7 +87,7 @@ function _highlightCard(card) { /** Trigger the tab's data load function (used when card wasn't found in DOM). */ function _triggerTabLoad(tab) { if (tab === 'dashboard' && typeof window.loadDashboard === 'function') window.loadDashboard(); - else if (tab === 'profiles' && typeof window.loadProfiles === 'function') window.loadProfiles(); + else if (tab === 'automations' && typeof window.loadAutomations === 'function') window.loadAutomations(); else if (tab === 'streams' && typeof window.loadPictureSources === 'function') window.loadPictureSources(); else if (tab === 'targets' && typeof window.loadTargetsTab === 'function') window.loadTargetsTab(); else if (tab === 'scenes' && typeof window.loadScenes === 'function') window.loadScenes(); diff --git a/server/src/wled_controller/static/js/core/state.js b/server/src/wled_controller/static/js/core/state.js index 5d01e0a..0960d3e 100644 --- a/server/src/wled_controller/static/js/core/state.js +++ b/server/src/wled_controller/static/js/core/state.js @@ -128,8 +128,8 @@ export function set_dashboardLoading(v) { _dashboardLoading = v; } export let _sourcesLoading = false; export function set_sourcesLoading(v) { _sourcesLoading = v; } -export let _profilesLoading = false; -export function set_profilesLoading(v) { _profilesLoading = v; } +export let _automationsLoading = false; +export function set_automationsLoading(v) { _automationsLoading = v; } // Dashboard poll interval (ms), persisted in localStorage const _POLL_KEY = 'dashboard_poll_interval'; @@ -195,6 +195,6 @@ export function set_audioTemplateNameManuallyEdited(v) { _audioTemplateNameManua export let _cachedValueSources = []; export function set_cachedValueSources(v) { _cachedValueSources = v; } -// Profiles -export let _profilesCache = null; -export function set_profilesCache(v) { _profilesCache = v; } +// Automations +export let _automationsCache = null; +export function set_automationsCache(v) { _automationsCache = v; } diff --git a/server/src/wled_controller/static/js/features/profiles.js b/server/src/wled_controller/static/js/features/automations.js similarity index 58% rename from server/src/wled_controller/static/js/features/profiles.js rename to server/src/wled_controller/static/js/features/automations.js index 7e9943e..87bf0f3 100644 --- a/server/src/wled_controller/static/js/features/profiles.js +++ b/server/src/wled_controller/static/js/features/automations.js @@ -1,199 +1,199 @@ /** - * Profiles — profile cards, editor, condition builder, process picker, scene selector. + * Automations — automation cards, editor, condition builder, process picker, scene selector. */ -import { apiKey, _profilesCache, set_profilesCache, _profilesLoading, set_profilesLoading } from '../core/state.js'; +import { apiKey, _automationsCache, set_automationsCache, _automationsLoading, set_automationsLoading } from '../core/state.js'; import { fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; import { showToast, showConfirm, setTabRefreshing } from '../core/ui.js'; import { Modal } from '../core/modal.js'; import { CardSection } from '../core/card-sections.js'; import { updateTabBadge } from './tabs.js'; -import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_PROFILE, 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'; // ===== Scene presets cache (shared by both selectors) ===== let _scenesCache = []; -class ProfileEditorModal extends Modal { - constructor() { super('profile-editor-modal'); } +class AutomationEditorModal extends Modal { + constructor() { super('automation-editor-modal'); } snapshotValues() { return { - name: document.getElementById('profile-editor-name').value, - enabled: document.getElementById('profile-editor-enabled').checked.toString(), - logic: document.getElementById('profile-editor-logic').value, - conditions: JSON.stringify(getProfileEditorConditions()), - scenePresetId: document.getElementById('profile-scene-id').value, - deactivationMode: document.getElementById('profile-deactivation-mode').value, - deactivationScenePresetId: document.getElementById('profile-fallback-scene-id').value, + name: document.getElementById('automation-editor-name').value, + enabled: document.getElementById('automation-editor-enabled').checked.toString(), + logic: document.getElementById('automation-editor-logic').value, + conditions: JSON.stringify(getAutomationEditorConditions()), + scenePresetId: document.getElementById('automation-scene-id').value, + deactivationMode: document.getElementById('automation-deactivation-mode').value, + deactivationScenePresetId: document.getElementById('automation-fallback-scene-id').value, }; } } -const profileModal = new ProfileEditorModal(); -const csProfiles = new CardSection('profiles', { titleKey: 'profiles.title', gridClass: 'devices-grid', addCardOnclick: "openProfileEditor()", keyAttr: 'data-profile-id' }); +const automationModal = new AutomationEditorModal(); +const csAutomations = new CardSection('automations', { titleKey: 'automations.title', gridClass: 'devices-grid', addCardOnclick: "openAutomationEditor()", keyAttr: 'data-automation-id' }); -// Re-render profiles when language changes (only if tab is active) +// Re-render automations when language changes (only if tab is active) document.addEventListener('languageChanged', () => { - if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'profiles') loadProfiles(); + if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'automations') loadAutomations(); }); -// React to real-time profile state changes from global events WS -document.addEventListener('server:profile_state_changed', () => { - if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'profiles') { - loadProfiles(); +// React to real-time automation state changes from global events WS +document.addEventListener('server:automation_state_changed', () => { + if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'automations') { + loadAutomations(); } }); -export async function loadProfiles() { - if (_profilesLoading) return; - set_profilesLoading(true); - const container = document.getElementById('profiles-content'); - if (!container) { set_profilesLoading(false); return; } - setTabRefreshing('profiles-content', true); +export async function loadAutomations() { + if (_automationsLoading) return; + set_automationsLoading(true); + const container = document.getElementById('automations-content'); + if (!container) { set_automationsLoading(false); return; } + setTabRefreshing('automations-content', true); try { - const [profilesResp, scenesResp] = await Promise.all([ - fetchWithAuth('/profiles'), + const [automationsResp, scenesResp] = await Promise.all([ + fetchWithAuth('/automations'), fetchWithAuth('/scene-presets'), ]); - if (!profilesResp.ok) throw new Error('Failed to load profiles'); - const data = await profilesResp.json(); + if (!automationsResp.ok) throw new Error('Failed to load automations'); + const data = await automationsResp.json(); const scenesData = scenesResp.ok ? await scenesResp.json() : { presets: [] }; _scenesCache = scenesData.presets || []; // Build scene name map for card rendering const sceneMap = new Map(_scenesCache.map(s => [s.id, s])); - set_profilesCache(data.profiles); - const activeCount = data.profiles.filter(p => p.is_active).length; - updateTabBadge('profiles', activeCount); - renderProfiles(data.profiles, sceneMap); + set_automationsCache(data.automations); + const activeCount = data.automations.filter(a => a.is_active).length; + updateTabBadge('automations', activeCount); + renderAutomations(data.automations, sceneMap); } catch (error) { if (error.isAuth) return; - console.error('Failed to load profiles:', error); + console.error('Failed to load automations:', error); container.innerHTML = `

${error.message}

`; } finally { - set_profilesLoading(false); - setTabRefreshing('profiles-content', false); + set_automationsLoading(false); + setTabRefreshing('automations-content', false); } } -export function expandAllProfileSections() { - CardSection.expandAll([csProfiles]); +export function expandAllAutomationSections() { + CardSection.expandAll([csAutomations]); } -export function collapseAllProfileSections() { - CardSection.collapseAll([csProfiles]); +export function collapseAllAutomationSections() { + CardSection.collapseAll([csAutomations]); } -function renderProfiles(profiles, sceneMap) { - const container = document.getElementById('profiles-content'); +function renderAutomations(automations, sceneMap) { + const container = document.getElementById('automations-content'); - const items = csProfiles.applySortOrder(profiles.map(p => ({ key: p.id, html: createProfileCard(p, sceneMap) }))); - const toolbar = `
`; - container.innerHTML = toolbar + csProfiles.render(items); - csProfiles.bind(); + const items = csAutomations.applySortOrder(automations.map(a => ({ key: a.id, html: createAutomationCard(a, sceneMap) }))); + const toolbar = `
`; + container.innerHTML = toolbar + csAutomations.render(items); + csAutomations.bind(); - // Localize data-i18n elements within the profiles container only + // Localize data-i18n elements within the automations container only container.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.getAttribute('data-i18n')); }); } -function createProfileCard(profile, sceneMap = new Map()) { - const statusClass = !profile.enabled ? 'disabled' : profile.is_active ? 'active' : 'inactive'; - const statusText = !profile.enabled ? t('profiles.status.disabled') : profile.is_active ? t('profiles.status.active') : t('profiles.status.inactive'); +function createAutomationCard(automation, sceneMap = new Map()) { + const statusClass = !automation.enabled ? 'disabled' : automation.is_active ? 'active' : 'inactive'; + const statusText = !automation.enabled ? t('automations.status.disabled') : automation.is_active ? t('automations.status.active') : t('automations.status.inactive'); let condPills = ''; - if (profile.conditions.length === 0) { - condPills = `${t('profiles.conditions.empty')}`; + if (automation.conditions.length === 0) { + condPills = `${t('automations.conditions.empty')}`; } else { - const parts = profile.conditions.map(c => { + const parts = automation.conditions.map(c => { if (c.condition_type === 'always') { - return `${ICON_OK} ${t('profiles.condition.always')}`; + return `${ICON_OK} ${t('automations.condition.always')}`; } if (c.condition_type === 'application') { const apps = (c.apps || []).join(', '); - const matchLabel = t('profiles.condition.application.match_type.' + (c.match_type || 'running')); - return `${t('profiles.condition.application')}: ${apps} (${matchLabel})`; + const matchLabel = t('automations.condition.application.match_type.' + (c.match_type || 'running')); + return `${t('automations.condition.application')}: ${apps} (${matchLabel})`; } if (c.condition_type === 'time_of_day') { return `${ICON_CLOCK} ${c.start_time || '00:00'} – ${c.end_time || '23:59'}`; } if (c.condition_type === 'system_idle') { - const mode = c.when_idle !== false ? t('profiles.condition.system_idle.when_idle') : t('profiles.condition.system_idle.when_active'); + const mode = c.when_idle !== false ? t('automations.condition.system_idle.when_idle') : t('automations.condition.system_idle.when_active'); return `${ICON_TIMER} ${c.idle_minutes || 5}m (${mode})`; } if (c.condition_type === 'display_state') { - const stateLabel = t('profiles.condition.display_state.' + (c.state || 'on')); - return `${ICON_MONITOR} ${t('profiles.condition.display_state')}: ${stateLabel}`; + const stateLabel = t('automations.condition.display_state.' + (c.state || 'on')); + return `${ICON_MONITOR} ${t('automations.condition.display_state')}: ${stateLabel}`; } if (c.condition_type === 'mqtt') { - return `${ICON_RADIO} ${t('profiles.condition.mqtt')}: ${escapeHtml(c.topic || '')} = ${escapeHtml(c.payload || '*')}`; + return `${ICON_RADIO} ${t('automations.condition.mqtt')}: ${escapeHtml(c.topic || '')} = ${escapeHtml(c.payload || '*')}`; } return `${c.condition_type}`; }); - const logicLabel = profile.condition_logic === 'and' ? t('profiles.logic.and') : t('profiles.logic.or'); - condPills = parts.join(`${logicLabel}`); + const logicLabel = automation.condition_logic === 'and' ? t('automations.logic.and') : t('automations.logic.or'); + condPills = parts.join(`${logicLabel}`); } // Scene info - const scene = profile.scene_preset_id ? sceneMap.get(profile.scene_preset_id) : null; - const sceneName = scene ? escapeHtml(scene.name) : t('profiles.scene.none_selected'); + const scene = automation.scene_preset_id ? sceneMap.get(automation.scene_preset_id) : null; + const sceneName = scene ? escapeHtml(scene.name) : t('automations.scene.none_selected'); const sceneColor = scene ? scene.color || '#4fc3f7' : '#888'; // Deactivation mode label let deactivationLabel = ''; - if (profile.deactivation_mode === 'revert') { - deactivationLabel = t('profiles.deactivation_mode.revert'); - } else if (profile.deactivation_mode === 'fallback_scene') { - const fallback = profile.deactivation_scene_preset_id ? sceneMap.get(profile.deactivation_scene_preset_id) : null; - deactivationLabel = fallback ? `${t('profiles.deactivation_mode.fallback_scene')}: ${escapeHtml(fallback.name)}` : t('profiles.deactivation_mode.fallback_scene'); + if (automation.deactivation_mode === 'revert') { + deactivationLabel = t('automations.deactivation_mode.revert'); + } else if (automation.deactivation_mode === 'fallback_scene') { + const fallback = automation.deactivation_scene_preset_id ? sceneMap.get(automation.deactivation_scene_preset_id) : null; + deactivationLabel = fallback ? `${t('automations.deactivation_mode.fallback_scene')}: ${escapeHtml(fallback.name)}` : t('automations.deactivation_mode.fallback_scene'); } let lastActivityMeta = ''; - if (profile.last_activated_at) { - const ts = new Date(profile.last_activated_at); - lastActivityMeta = `${ICON_CLOCK} ${ts.toLocaleString()}`; + if (automation.last_activated_at) { + const ts = new Date(automation.last_activated_at); + lastActivityMeta = `${ICON_CLOCK} ${ts.toLocaleString()}`; } return ` -
+
- +
- ${escapeHtml(profile.name)} - ${statusText} + ${escapeHtml(automation.name)} + ${statusText}
- ${profile.condition_logic === 'and' ? t('profiles.logic.all') : t('profiles.logic.any')} + ${automation.condition_logic === 'and' ? t('automations.logic.all') : t('automations.logic.any')} ${ICON_SCENE} ${sceneName} ${deactivationLabel ? `${deactivationLabel}` : ''} ${lastActivityMeta}
${condPills}
- - +
`; } -export async function openProfileEditor(profileId) { - const modal = document.getElementById('profile-editor-modal'); - const titleEl = document.getElementById('profile-editor-title'); - const idInput = document.getElementById('profile-editor-id'); - const nameInput = document.getElementById('profile-editor-name'); - const enabledInput = document.getElementById('profile-editor-enabled'); - const logicSelect = document.getElementById('profile-editor-logic'); - const condList = document.getElementById('profile-conditions-list'); - const errorEl = document.getElementById('profile-editor-error'); +export async function openAutomationEditor(automationId) { + const modal = document.getElementById('automation-editor-modal'); + const titleEl = document.getElementById('automation-editor-title'); + const idInput = document.getElementById('automation-editor-id'); + const nameInput = document.getElementById('automation-editor-name'); + const enabledInput = document.getElementById('automation-editor-enabled'); + const logicSelect = document.getElementById('automation-editor-logic'); + const condList = document.getElementById('automation-conditions-list'); + const errorEl = document.getElementById('automation-editor-error'); errorEl.style.display = 'none'; condList.innerHTML = ''; @@ -208,66 +208,66 @@ export async function openProfileEditor(profileId) { } catch { /* use cached */ } // Reset deactivation mode - document.getElementById('profile-deactivation-mode').value = 'none'; - document.getElementById('profile-fallback-scene-group').style.display = 'none'; + document.getElementById('automation-deactivation-mode').value = 'none'; + document.getElementById('automation-fallback-scene-group').style.display = 'none'; - if (profileId) { - titleEl.innerHTML = `${ICON_PROFILE} ${t('profiles.edit')}`; + if (automationId) { + titleEl.innerHTML = `${ICON_AUTOMATION} ${t('automations.edit')}`; try { - const resp = await fetchWithAuth(`/profiles/${profileId}`); - if (!resp.ok) throw new Error('Failed to load profile'); - const profile = await resp.json(); + const resp = await fetchWithAuth(`/automations/${automationId}`); + if (!resp.ok) throw new Error('Failed to load automation'); + const automation = await resp.json(); - idInput.value = profile.id; - nameInput.value = profile.name; - enabledInput.checked = profile.enabled; - logicSelect.value = profile.condition_logic; + idInput.value = automation.id; + nameInput.value = automation.name; + enabledInput.checked = automation.enabled; + logicSelect.value = automation.condition_logic; - for (const c of profile.conditions) { - addProfileConditionRow(c); + for (const c of automation.conditions) { + addAutomationConditionRow(c); } // Scene selector - _initSceneSelector('profile-scene', profile.scene_preset_id); + _initSceneSelector('automation-scene', automation.scene_preset_id); // Deactivation mode - document.getElementById('profile-deactivation-mode').value = profile.deactivation_mode || 'none'; + document.getElementById('automation-deactivation-mode').value = automation.deactivation_mode || 'none'; _onDeactivationModeChange(); - _initSceneSelector('profile-fallback-scene', profile.deactivation_scene_preset_id); + _initSceneSelector('automation-fallback-scene', automation.deactivation_scene_preset_id); } catch (e) { showToast(e.message, 'error'); return; } } else { - titleEl.innerHTML = `${ICON_PROFILE} ${t('profiles.add')}`; + titleEl.innerHTML = `${ICON_AUTOMATION} ${t('automations.add')}`; idInput.value = ''; nameInput.value = ''; enabledInput.checked = true; logicSelect.value = 'or'; - _initSceneSelector('profile-scene', null); - _initSceneSelector('profile-fallback-scene', null); + _initSceneSelector('automation-scene', null); + _initSceneSelector('automation-fallback-scene', null); } // Wire up deactivation mode change - document.getElementById('profile-deactivation-mode').onchange = _onDeactivationModeChange; + document.getElementById('automation-deactivation-mode').onchange = _onDeactivationModeChange; - profileModal.open(); + automationModal.open(); modal.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.getAttribute('data-i18n')); }); modal.querySelectorAll('[data-i18n-placeholder]').forEach(el => { el.placeholder = t(el.getAttribute('data-i18n-placeholder')); }); - profileModal.snapshot(); + automationModal.snapshot(); } function _onDeactivationModeChange() { - const mode = document.getElementById('profile-deactivation-mode').value; - document.getElementById('profile-fallback-scene-group').style.display = mode === 'fallback_scene' ? '' : 'none'; + const mode = document.getElementById('automation-deactivation-mode').value; + document.getElementById('automation-fallback-scene-group').style.display = mode === 'fallback_scene' ? '' : 'none'; } -export async function closeProfileEditorModal() { - await profileModal.close(); +export async function closeAutomationEditorModal() { + await automationModal.close(); } // ===== Scene selector logic ===== @@ -296,7 +296,7 @@ function _initSceneSelector(prefix, selectedId) { const filtered = query ? _scenesCache.filter(s => s.name.toLowerCase().includes(query)) : _scenesCache; if (filtered.length === 0) { - dropdown.innerHTML = `
${t('profiles.scene.none_available')}
`; + dropdown.innerHTML = `
${t('automations.scene.none_available')}
`; } else { dropdown.innerHTML = filtered.map(s => { const selected = s.id === hiddenInput.value ? ' selected' : ''; @@ -370,27 +370,27 @@ function _initSceneSelector(prefix, selectedId) { // ===== Condition editor ===== -export function addProfileCondition() { - addProfileConditionRow({ condition_type: 'application', apps: [], match_type: 'running' }); +export function addAutomationCondition() { + addAutomationConditionRow({ condition_type: 'application', apps: [], match_type: 'running' }); } -function addProfileConditionRow(condition) { - const list = document.getElementById('profile-conditions-list'); +function addAutomationConditionRow(condition) { + const list = document.getElementById('automation-conditions-list'); const row = document.createElement('div'); - row.className = 'profile-condition-row'; + row.className = 'automation-condition-row'; const condType = condition.condition_type || 'application'; row.innerHTML = `
- +
`; @@ -400,7 +400,7 @@ function addProfileConditionRow(condition) { function renderFields(type, data) { if (type === 'always') { - container.innerHTML = `${t('profiles.condition.always.hint')}`; + container.innerHTML = `${t('automations.condition.always.hint')}`; return; } if (type === 'time_of_day') { @@ -409,14 +409,14 @@ function addProfileConditionRow(condition) { container.innerHTML = `
- +
- +
- ${t('profiles.condition.time_of_day.overnight_hint')} + ${t('automations.condition.time_of_day.overnight_hint')}
`; return; } @@ -426,14 +426,14 @@ function addProfileConditionRow(condition) { container.innerHTML = `
- +
- +
`; @@ -444,10 +444,10 @@ function addProfileConditionRow(condition) { container.innerHTML = `
- +
`; @@ -460,19 +460,19 @@ function addProfileConditionRow(condition) { container.innerHTML = `
- +
- +
- +
`; @@ -483,22 +483,22 @@ function addProfileConditionRow(condition) { container.innerHTML = `
- +
- - + +
@@ -551,7 +551,7 @@ async function toggleProcessPicker(picker, row) { function renderProcessPicker(picker, processes, existing) { const listEl = picker.querySelector('.process-picker-list'); if (processes.length === 0) { - listEl.innerHTML = `
${t('profiles.condition.application.no_processes')}
`; + listEl.innerHTML = `
${t('automations.condition.application.no_processes')}
`; return; } listEl.innerHTML = processes.map(p => { @@ -562,7 +562,7 @@ function renderProcessPicker(picker, processes, existing) { listEl.querySelectorAll('.process-picker-item:not(.added)').forEach(item => { item.addEventListener('click', () => { const proc = item.dataset.process; - const row = picker.closest('.profile-condition-row'); + const row = picker.closest('.automation-condition-row'); const textarea = row.querySelector('.condition-apps'); const current = textarea.value.trim(); textarea.value = current ? current + '\n' + proc : proc; @@ -579,8 +579,8 @@ function filterProcessPicker(picker) { renderProcessPicker(picker, filtered, picker._existing || new Set()); } -function getProfileEditorConditions() { - const rows = document.querySelectorAll('#profile-conditions-list .profile-condition-row'); +function getAutomationEditorConditions() { + const rows = document.querySelectorAll('#automation-conditions-list .automation-condition-row'); const conditions = []; rows.forEach(row => { const typeSelect = row.querySelector('.condition-type-select'); @@ -621,15 +621,15 @@ function getProfileEditorConditions() { return conditions; } -export async function saveProfileEditor() { - const idInput = document.getElementById('profile-editor-id'); - const nameInput = document.getElementById('profile-editor-name'); - const enabledInput = document.getElementById('profile-editor-enabled'); - const logicSelect = document.getElementById('profile-editor-logic'); +export async function saveAutomationEditor() { + const idInput = document.getElementById('automation-editor-id'); + const nameInput = document.getElementById('automation-editor-name'); + const enabledInput = document.getElementById('automation-editor-enabled'); + const logicSelect = document.getElementById('automation-editor-logic'); const name = nameInput.value.trim(); if (!name) { - profileModal.showError(t('profiles.error.name_required')); + automationModal.showError(t('automations.error.name_required')); return; } @@ -637,61 +637,61 @@ export async function saveProfileEditor() { name, enabled: enabledInput.checked, condition_logic: logicSelect.value, - conditions: getProfileEditorConditions(), - scene_preset_id: document.getElementById('profile-scene-id').value || null, - deactivation_mode: document.getElementById('profile-deactivation-mode').value, - deactivation_scene_preset_id: document.getElementById('profile-fallback-scene-id').value || null, + conditions: getAutomationEditorConditions(), + scene_preset_id: document.getElementById('automation-scene-id').value || null, + deactivation_mode: document.getElementById('automation-deactivation-mode').value, + deactivation_scene_preset_id: document.getElementById('automation-fallback-scene-id').value || null, }; - const profileId = idInput.value; - const isEdit = !!profileId; + const automationId = idInput.value; + const isEdit = !!automationId; try { - const url = isEdit ? `/profiles/${profileId}` : '/profiles'; + const url = isEdit ? `/automations/${automationId}` : '/automations'; const resp = await fetchWithAuth(url, { method: isEdit ? 'PUT' : 'POST', body: JSON.stringify(body), }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); - throw new Error(err.detail || 'Failed to save profile'); + throw new Error(err.detail || 'Failed to save automation'); } - profileModal.forceClose(); - showToast(isEdit ? t('profiles.updated') : t('profiles.created'), 'success'); - loadProfiles(); + automationModal.forceClose(); + showToast(isEdit ? t('automations.updated') : t('automations.created'), 'success'); + loadAutomations(); } catch (e) { if (e.isAuth) return; - profileModal.showError(e.message); + automationModal.showError(e.message); } } -export async function toggleProfileEnabled(profileId, enable) { +export async function toggleAutomationEnabled(automationId, enable) { try { const action = enable ? 'enable' : 'disable'; - const resp = await fetchWithAuth(`/profiles/${profileId}/${action}`, { + const resp = await fetchWithAuth(`/automations/${automationId}/${action}`, { method: 'POST', }); - if (!resp.ok) throw new Error(`Failed to ${action} profile`); - loadProfiles(); + if (!resp.ok) throw new Error(`Failed to ${action} automation`); + loadAutomations(); } catch (e) { if (e.isAuth) return; showToast(e.message, 'error'); } } -export async function deleteProfile(profileId, profileName) { - const msg = t('profiles.delete.confirm').replace('{name}', profileName); +export async function deleteAutomation(automationId, automationName) { + const msg = t('automations.delete.confirm').replace('{name}', automationName); const confirmed = await showConfirm(msg); if (!confirmed) return; try { - const resp = await fetchWithAuth(`/profiles/${profileId}`, { + const resp = await fetchWithAuth(`/automations/${automationId}`, { method: 'DELETE', }); - if (!resp.ok) throw new Error('Failed to delete profile'); - showToast(t('profiles.deleted'), 'success'); - loadProfiles(); + if (!resp.ok) throw new Error('Failed to delete automation'); + showToast(t('automations.deleted'), 'success'); + loadAutomations(); } catch (e) { if (e.isAuth) return; showToast(e.message, 'error'); diff --git a/server/src/wled_controller/static/js/features/dashboard.js b/server/src/wled_controller/static/js/features/dashboard.js index 2696fe8..33ae8a3 100644 --- a/server/src/wled_controller/static/js/features/dashboard.js +++ b/server/src/wled_controller/static/js/features/dashboard.js @@ -10,7 +10,7 @@ import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling } import { startAutoRefresh, updateTabBadge } from './tabs.js'; import { getTargetTypeIcon, - ICON_TARGET, ICON_PROFILE, ICON_CLOCK, ICON_WARNING, ICON_OK, + ICON_TARGET, ICON_AUTOMATION, ICON_CLOCK, ICON_WARNING, ICON_OK, ICON_STOP, ICON_STOP_PLAIN, ICON_START, ICON_AUTOSTART, ICON_HELP, ICON_SCENE, } from '../core/icons.js'; import { loadScenePresets, renderScenePresetsSection } from './scene-presets.js'; @@ -252,28 +252,28 @@ function _updateRunningMetrics(enrichedRunning) { } -function _updateProfilesInPlace(profiles) { - for (const p of profiles) { - const card = document.querySelector(`[data-profile-id="${p.id}"]`); +function _updateAutomationsInPlace(automations) { + for (const a of automations) { + const card = document.querySelector(`[data-automation-id="${a.id}"]`); if (!card) continue; const badge = card.querySelector('.dashboard-badge-active, .dashboard-badge-stopped'); if (badge) { - if (!p.enabled) { + if (!a.enabled) { badge.className = 'dashboard-badge-stopped'; - badge.textContent = t('profiles.status.disabled'); - } else if (p.is_active) { + badge.textContent = t('automations.status.disabled'); + } else if (a.is_active) { badge.className = 'dashboard-badge-active'; - badge.textContent = t('profiles.status.active'); + badge.textContent = t('automations.status.active'); } else { badge.className = 'dashboard-badge-stopped'; - badge.textContent = t('profiles.status.inactive'); + badge.textContent = t('automations.status.inactive'); } } const btn = card.querySelector('.dashboard-target-actions .dashboard-action-btn'); if (btn) { - btn.className = `dashboard-action-btn ${p.enabled ? 'stop' : 'start'}`; - btn.setAttribute('onclick', `dashboardToggleProfile('${p.id}', ${!p.enabled})`); - btn.innerHTML = p.enabled ? ICON_STOP_PLAIN : ICON_START; + btn.className = `dashboard-action-btn ${a.enabled ? 'stop' : 'start'}`; + btn.setAttribute('onclick', `dashboardToggleAutomation('${a.id}', ${!a.enabled})`); + btn.innerHTML = a.enabled ? ICON_STOP_PLAIN : ICON_START; } } } @@ -368,9 +368,9 @@ export async function loadDashboard(forceFullRender = false) { try { // Fire all requests in a single batch to avoid sequential RTTs - const [targetsResp, profilesResp, devicesResp, cssResp, batchStatesResp, batchMetricsResp, scenePresets] = await Promise.all([ + const [targetsResp, automationsResp, devicesResp, cssResp, batchStatesResp, batchMetricsResp, scenePresets] = await Promise.all([ fetchWithAuth('/picture-targets'), - fetchWithAuth('/profiles').catch(() => null), + fetchWithAuth('/automations').catch(() => null), fetchWithAuth('/devices').catch(() => null), fetchWithAuth('/color-strip-sources').catch(() => null), fetchWithAuth('/picture-targets/batch/states').catch(() => null), @@ -380,8 +380,8 @@ export async function loadDashboard(forceFullRender = false) { const targetsData = await targetsResp.json(); const targets = targetsData.targets || []; - const profilesData = profilesResp && profilesResp.ok ? await profilesResp.json() : { profiles: [] }; - const profiles = profilesData.profiles || []; + const automationsData = automationsResp && automationsResp.ok ? await automationsResp.json() : { automations: [] }; + const automations = automationsData.automations || []; const devicesData = devicesResp && devicesResp.ok ? await devicesResp.json() : { devices: [] }; const devicesMap = {}; for (const d of (devicesData.devices || [])) { devicesMap[d.id] = d; } @@ -392,12 +392,12 @@ export async function loadDashboard(forceFullRender = false) { const allStates = batchStatesResp && batchStatesResp.ok ? (await batchStatesResp.json()).states : {}; const allMetrics = batchMetricsResp && batchMetricsResp.ok ? (await batchMetricsResp.json()).metrics : {}; - // Build dynamic HTML (targets, profiles) + // Build dynamic HTML (targets, automations) let dynamicHtml = ''; let runningIds = []; let newAutoStartIds = ''; - if (targets.length === 0 && profiles.length === 0 && scenePresets.length === 0) { + if (targets.length === 0 && automations.length === 0 && scenePresets.length === 0) { dynamicHtml = `
${t('dashboard.no_targets')}
`; } else { const enriched = targets.map(target => ({ @@ -426,7 +426,7 @@ export async function loadDashboard(forceFullRender = false) { } if (structureUnchanged && forceFullRender) { if (running.length > 0) _updateRunningMetrics(running); - _updateProfilesInPlace(profiles); + _updateAutomationsInPlace(automations); _cacheUptimeElements(); _startUptimeTimer(); startPerfPolling(); @@ -451,8 +451,8 @@ export async function loadDashboard(forceFullRender = false) { } } const statusBadge = isRunning - ? `${t('profiles.status.active')}` - : `${t('profiles.status.inactive')}`; + ? `${t('automations.status.active')}` + : `${t('automations.status.inactive')}`; const subtitle = subtitleParts.length ? `
${escapeHtml(subtitleParts.join(' · '))}
` : ''; const asNavSub = isLed ? 'led' : 'key_colors'; const asNavSec = isLed ? 'led-targets' : 'kc-targets'; @@ -480,16 +480,16 @@ export async function loadDashboard(forceFullRender = false) {
`; } - if (profiles.length > 0) { - const activeProfiles = profiles.filter(p => p.is_active); - const inactiveProfiles = profiles.filter(p => !p.is_active); - updateTabBadge('profiles', activeProfiles.length); + if (automations.length > 0) { + const activeAutomations = automations.filter(a => a.is_active); + const inactiveAutomations = automations.filter(a => !a.is_active); + updateTabBadge('automations', activeAutomations.length); const sceneMap = new Map(scenePresets.map(s => [s.id, s])); - const profileItems = [...activeProfiles, ...inactiveProfiles].map(p => renderDashboardProfile(p, sceneMap)).join(''); + const automationItems = [...activeAutomations, ...inactiveAutomations].map(a => renderDashboardAutomation(a, sceneMap)).join(''); dynamicHtml += `
- ${_sectionHeader('profiles', t('dashboard.section.profiles'), profiles.length)} - ${_sectionContent('profiles', profileItems)} + ${_sectionHeader('automations', t('dashboard.section.automations'), automations.length)} + ${_sectionContent('automations', automationItems)}
`; } @@ -664,56 +664,56 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap } } -function renderDashboardProfile(profile, sceneMap = new Map()) { - const isActive = profile.is_active; - const isDisabled = !profile.enabled; +function renderDashboardAutomation(automation, sceneMap = new Map()) { + const isActive = automation.is_active; + const isDisabled = !automation.enabled; let condSummary = ''; - if (profile.conditions.length > 0) { - const parts = profile.conditions.map(c => { + if (automation.conditions.length > 0) { + const parts = automation.conditions.map(c => { if (c.condition_type === 'application') { const apps = (c.apps || []).join(', '); - const matchLabel = c.match_type === 'topmost' ? t('profiles.condition.application.match_type.topmost') : t('profiles.condition.application.match_type.running'); + const matchLabel = c.match_type === 'topmost' ? t('automations.condition.application.match_type.topmost') : t('automations.condition.application.match_type.running'); return `${apps} (${matchLabel})`; } return c.condition_type; }); - const logic = profile.condition_logic === 'and' ? ' & ' : ' | '; + const logic = automation.condition_logic === 'and' ? ' & ' : ' | '; condSummary = parts.join(logic); } const statusBadge = isDisabled - ? `${t('profiles.status.disabled')}` + ? `${t('automations.status.disabled')}` : isActive - ? `${t('profiles.status.active')}` - : `${t('profiles.status.inactive')}`; + ? `${t('automations.status.active')}` + : `${t('automations.status.inactive')}`; // Scene info - const scene = profile.scene_preset_id ? sceneMap.get(profile.scene_preset_id) : null; - const sceneName = scene ? escapeHtml(scene.name) : t('profiles.scene.none_selected'); + const scene = automation.scene_preset_id ? sceneMap.get(automation.scene_preset_id) : null; + const sceneName = scene ? escapeHtml(scene.name) : t('automations.scene.none_selected'); - return `