Add profile conditions, scene presets, MQTT integration, and Scenes tab

Feature 1 — Profile Conditions: time-of-day, system idle (Win32
GetLastInputInfo), and display state (GUID_CONSOLE_DISPLAY_STATE)
condition types for automatic profile activation.

Feature 2 — Scene Presets: snapshot/restore system that captures target
running states, device brightness, and profile enables. Server-side
capture with 5-step activation order. Dedicated Scenes tab with
CardSection-based card grid, command palette integration, and dashboard
quick-activate section.

Feature 3 — MQTT Integration: MQTTService singleton with aiomqtt,
MQTTLEDClient device provider for pixel output, MQTT profile condition
type with topic/payload matching, and frontend support for MQTT device
type and condition editor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 16:57:42 +03:00
parent bd8d7a019f
commit 2e747b5ece
38 changed files with 2269 additions and 32 deletions
@@ -0,0 +1,125 @@
"""Scene preset data models — snapshot of current system state."""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, List, Optional
@dataclass
class TargetSnapshot:
"""Snapshot of a single target's mutable state."""
target_id: str
running: bool = False
color_strip_source_id: str = ""
brightness_value_source_id: str = ""
fps: int = 30
auto_start: bool = False
def to_dict(self) -> dict:
return {
"target_id": self.target_id,
"running": self.running,
"color_strip_source_id": self.color_strip_source_id,
"brightness_value_source_id": self.brightness_value_source_id,
"fps": self.fps,
"auto_start": self.auto_start,
}
@classmethod
def from_dict(cls, data: dict) -> "TargetSnapshot":
return cls(
target_id=data["target_id"],
running=data.get("running", False),
color_strip_source_id=data.get("color_strip_source_id", ""),
brightness_value_source_id=data.get("brightness_value_source_id", ""),
fps=data.get("fps", 30),
auto_start=data.get("auto_start", False),
)
@dataclass
class DeviceBrightnessSnapshot:
"""Snapshot of a device's software brightness."""
device_id: str
software_brightness: int = 255
def to_dict(self) -> dict:
return {
"device_id": self.device_id,
"software_brightness": self.software_brightness,
}
@classmethod
def from_dict(cls, data: dict) -> "DeviceBrightnessSnapshot":
return cls(
device_id=data["device_id"],
software_brightness=data.get("software_brightness", 255),
)
@dataclass
class ProfileSnapshot:
"""Snapshot of a profile's enabled state."""
profile_id: str
enabled: bool = True
def to_dict(self) -> dict:
return {
"profile_id": self.profile_id,
"enabled": self.enabled,
}
@classmethod
def from_dict(cls, data: dict) -> "ProfileSnapshot":
return cls(
profile_id=data["profile_id"],
enabled=data.get("enabled", True),
)
@dataclass
class ScenePreset:
"""A named snapshot of system state that can be restored."""
id: str
name: str
description: str = ""
color: str = "#4fc3f7" # accent color for the card
targets: List[TargetSnapshot] = field(default_factory=list)
devices: List[DeviceBrightnessSnapshot] = field(default_factory=list)
profiles: List[ProfileSnapshot] = field(default_factory=list)
order: int = 0
created_at: datetime = field(default_factory=datetime.utcnow)
updated_at: datetime = field(default_factory=datetime.utcnow)
def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"description": self.description,
"color": self.color,
"targets": [t.to_dict() for t in self.targets],
"devices": [d.to_dict() for d in self.devices],
"profiles": [p.to_dict() for p in self.profiles],
"order": self.order,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
}
@classmethod
def from_dict(cls, data: dict) -> "ScenePreset":
return cls(
id=data["id"],
name=data["name"],
description=data.get("description", ""),
color=data.get("color", "#4fc3f7"),
targets=[TargetSnapshot.from_dict(t) for t in data.get("targets", [])],
devices=[DeviceBrightnessSnapshot.from_dict(d) for d in data.get("devices", [])],
profiles=[ProfileSnapshot.from_dict(p) for p in data.get("profiles", [])],
order=data.get("order", 0),
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
)