Replace profile targets with scene activation and searchable scene selector

Profiles now activate scene presets instead of individual targets, with
configurable deactivation behavior (none/revert/fallback scene). The
target checklist UI is replaced by a searchable combobox for scene
selection that scales well with many scenes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 17:29:02 +03:00
parent 2e747b5ece
commit da3e53e1f1
17 changed files with 739 additions and 353 deletions

View File

@@ -160,14 +160,16 @@ class MQTTCondition(Condition):
@dataclass
class Profile:
"""Automation profile that activates targets based on conditions."""
"""Automation profile that activates a scene preset based on conditions."""
id: str
name: str
enabled: bool
condition_logic: str # "or" | "and"
conditions: List[Condition]
target_ids: List[str]
scene_preset_id: Optional[str] # scene to activate when conditions are met
deactivation_mode: str # "none" | "revert" | "fallback_scene"
deactivation_scene_preset_id: Optional[str] # scene for fallback_scene mode
created_at: datetime
updated_at: datetime
@@ -178,7 +180,9 @@ class Profile:
"enabled": self.enabled,
"condition_logic": self.condition_logic,
"conditions": [c.to_dict() for c in self.conditions],
"target_ids": list(self.target_ids),
"scene_preset_id": self.scene_preset_id,
"deactivation_mode": self.deactivation_mode,
"deactivation_scene_preset_id": self.deactivation_scene_preset_id,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
}
@@ -198,7 +202,9 @@ class Profile:
enabled=data.get("enabled", True),
condition_logic=data.get("condition_logic", "or"),
conditions=conditions,
target_ids=data.get("target_ids", []),
scene_preset_id=data.get("scene_preset_id"),
deactivation_mode=data.get("deactivation_mode", "none"),
deactivation_scene_preset_id=data.get("deactivation_scene_preset_id"),
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
)

View File

@@ -74,7 +74,9 @@ class ProfileStore:
enabled: bool = True,
condition_logic: str = "or",
conditions: Optional[List[Condition]] = None,
target_ids: Optional[List[str]] = None,
scene_preset_id: Optional[str] = None,
deactivation_mode: str = "none",
deactivation_scene_preset_id: Optional[str] = None,
) -> Profile:
for p in self._profiles.values():
if p.name == name:
@@ -89,7 +91,9 @@ class ProfileStore:
enabled=enabled,
condition_logic=condition_logic,
conditions=conditions or [],
target_ids=target_ids or [],
scene_preset_id=scene_preset_id,
deactivation_mode=deactivation_mode,
deactivation_scene_preset_id=deactivation_scene_preset_id,
created_at=now,
updated_at=now,
)
@@ -107,7 +111,9 @@ class ProfileStore:
enabled: Optional[bool] = None,
condition_logic: Optional[str] = None,
conditions: Optional[List[Condition]] = None,
target_ids: Optional[List[str]] = None,
scene_preset_id: str = "__unset__",
deactivation_mode: Optional[str] = None,
deactivation_scene_preset_id: str = "__unset__",
) -> Profile:
if profile_id not in self._profiles:
raise ValueError(f"Profile not found: {profile_id}")
@@ -125,8 +131,12 @@ class ProfileStore:
profile.condition_logic = condition_logic
if conditions is not None:
profile.conditions = conditions
if target_ids is not None:
profile.target_ids = target_ids
if scene_preset_id != "__unset__":
profile.scene_preset_id = scene_preset_id
if deactivation_mode is not None:
profile.deactivation_mode = deactivation_mode
if deactivation_scene_preset_id != "__unset__":
profile.deactivation_scene_preset_id = deactivation_scene_preset_id
profile.updated_at = datetime.utcnow()
self._save()