Add HAOS scene preset buttons and smooth tutorial scrolling

Expose scene presets as button entities in the HA integration under a
dedicated "Scenes" device. Each button activates its scene via the API.
The coordinator now fetches scene presets alongside other data, and the
integration reloads when the scene list changes.

Also animate tutorial autoscroll with smooth behavior and wait for
scrollend before positioning the spotlight overlay.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 19:53:47 +03:00
parent a34edf9650
commit 252db09145
7 changed files with 208 additions and 38 deletions

View File

@@ -28,6 +28,7 @@ from .ws_manager import KeyColorsWebSocketManager
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.SWITCH,
Platform.SENSOR,
Platform.NUMBER,
@@ -79,6 +80,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
current_identifiers.add((DOMAIN, target_id))
# Create a single "Scenes" device for scene preset buttons
scenes_identifier = (DOMAIN, f"{entry.entry_id}_scenes")
scene_presets = coordinator.data.get("scene_presets", []) if coordinator.data else []
if scene_presets:
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={scenes_identifier},
name=f"{server_name} Scenes",
manufacturer=server_name,
model="Scene Presets",
configuration_url=server_url,
)
current_identifiers.add(scenes_identifier)
# Remove devices for targets that no longer exist
for device_entry in dr.async_entries_for_config_entry(
device_registry, entry.entry_id
@@ -95,10 +110,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
DATA_EVENT_LISTENER: event_listener,
}
# Track target IDs to detect changes
# Track target and scene IDs to detect changes
initial_target_ids = set(
coordinator.data.get("targets", {}).keys() if coordinator.data else []
)
initial_scene_ids = set(
p["id"] for p in (coordinator.data.get("scene_presets", []) if coordinator.data else [])
)
def _on_coordinator_update() -> None:
"""Manage WS connections and detect target list changes."""
@@ -117,10 +135,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
else:
hass.async_create_task(ws_manager.stop_listening(target_id))
# Reload if target list changed
# Reload if target or scene list changed
current_ids = set(targets.keys())
if current_ids != initial_target_ids:
_LOGGER.info("Target list changed, reloading integration")
current_scene_ids = set(
p["id"] for p in coordinator.data.get("scene_presets", [])
)
if current_ids != initial_target_ids or current_scene_ids != initial_scene_ids:
_LOGGER.info("Target or scene list changed, reloading integration")
hass.async_create_task(
hass.config_entries.async_reload(entry.entry_id)
)

View File

@@ -0,0 +1,75 @@
"""Button platform for LED Screen Controller — scene preset activation."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, DATA_COORDINATOR
from .coordinator import WLEDScreenControllerCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up scene preset buttons."""
data = hass.data[DOMAIN][entry.entry_id]
coordinator: WLEDScreenControllerCoordinator = data[DATA_COORDINATOR]
entities = []
if coordinator.data:
for preset in coordinator.data.get("scene_presets", []):
entities.append(
SceneActivateButton(coordinator, preset, entry.entry_id)
)
async_add_entities(entities)
class SceneActivateButton(CoordinatorEntity, ButtonEntity):
"""Button that activates a scene preset."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: WLEDScreenControllerCoordinator,
preset: dict[str, Any],
entry_id: str,
) -> None:
"""Initialize the button."""
super().__init__(coordinator)
self._preset_id = preset["id"]
self._entry_id = entry_id
self._attr_unique_id = f"{entry_id}_scene_{preset['id']}"
self._attr_translation_key = "activate_scene"
self._attr_translation_placeholders = {"scene_name": preset["name"]}
self._attr_icon = "mdi:palette"
@property
def device_info(self) -> dict[str, Any]:
"""Return device information — all scene buttons belong to the Scenes device."""
return {"identifiers": {(DOMAIN, f"{self._entry_id}_scenes")}}
@property
def available(self) -> bool:
"""Return if entity is available."""
if not self.coordinator.data:
return False
return any(
p["id"] == self._preset_id
for p in self.coordinator.data.get("scene_presets", [])
)
async def async_press(self) -> None:
"""Activate the scene preset."""
await self.coordinator.activate_scene(self._preset_id)

View File

@@ -107,11 +107,14 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
target_id, data = r
targets_data[target_id] = data
# Fetch devices, CSS sources, and value sources in parallel
devices_data, css_sources, value_sources = await asyncio.gather(
self._fetch_devices(),
self._fetch_css_sources(),
self._fetch_value_sources(),
# Fetch devices, CSS sources, value sources, and scene presets in parallel
devices_data, css_sources, value_sources, scene_presets = (
await asyncio.gather(
self._fetch_devices(),
self._fetch_css_sources(),
self._fetch_value_sources(),
self._fetch_scene_presets(),
)
)
return {
@@ -119,6 +122,7 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
"devices": devices_data,
"css_sources": css_sources,
"value_sources": value_sources,
"scene_presets": scene_presets,
"server_version": self.server_version,
}
@@ -317,6 +321,37 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
_LOGGER.warning("Failed to fetch value sources: %s", err)
return []
async def _fetch_scene_presets(self) -> list[dict[str, Any]]:
"""Fetch all scene presets."""
try:
async with self.session.get(
f"{self.server_url}/api/v1/scene-presets",
headers=self._auth_headers,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as resp:
resp.raise_for_status()
data = await resp.json()
return data.get("presets", [])
except Exception as err:
_LOGGER.warning("Failed to fetch scene presets: %s", err)
return []
async def activate_scene(self, preset_id: str) -> None:
"""Activate a scene preset."""
async with self.session.post(
f"{self.server_url}/api/v1/scene-presets/{preset_id}/activate",
headers=self._auth_headers,
timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT),
) as resp:
if resp.status != 200:
body = await resp.text()
_LOGGER.error(
"Failed to activate scene %s: %s %s",
preset_id, resp.status, body,
)
resp.raise_for_status()
await self.async_request_refresh()
async def update_target(self, target_id: str, **kwargs: Any) -> None:
"""Update a picture target's fields."""
async with self.session.put(

View File

@@ -26,6 +26,11 @@
}
},
"entity": {
"button": {
"activate_scene": {
"name": "{scene_name}"
}
},
"switch": {
"processing": {
"name": "Processing"

View File

@@ -26,6 +26,11 @@
}
},
"entity": {
"button": {
"activate_scene": {
"name": "{scene_name}"
}
},
"switch": {
"processing": {
"name": "Processing"

View File

@@ -26,6 +26,11 @@
}
},
"entity": {
"button": {
"activate_scene": {
"name": "{scene_name}"
}
},
"switch": {
"processing": {
"name": "Обработка"