feat(playlists): expose scene playlists as Home Assistant entities

One device per scene playlist (model "Scene Playlist"), each with:
- a switch (start/stop; the server cycles one playlist at a time, so starting one stops any other)
- a "Current Scene" sensor that resolves the active preset name from scene_presets while this playlist is cycling
- an "Items" diagnostic sensor (configured scene count; no state_class)

The coordinator reads scene_playlists plus the flat playlist_state from the /api/v1/snapshot payload and gains start_playlist()/stop_playlist(); __init__ registers and prunes per-playlist devices and reloads on playlist-id changes; the event listener also refreshes on the playlist_state_changed WS event. Shared device/lookup/running-state plumbing lives in a new LedGrabPlaylistEntity base (entity.py) used by both the switch and the sensors. Adds en/ru translation keys.

Note: this also lands the in-progress coordinator migration from the per-request fan-out to the single /api/v1/snapshot poll that was already present in the working tree.

Requires the led-grab server /api/v1/snapshot to emit scene_playlists + playlist_state (companion server change, tracked separately).
This commit is contained in:
2026-06-08 15:59:08 +03:00
parent a666d9eb9c
commit 705616f8b0
9 changed files with 404 additions and 289 deletions
+29 -5
View File
@@ -113,6 +113,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
current_identifiers.add(scenes_identifier)
# One device per scene playlist — groups its switch + stats sensors.
scene_playlists = (
coordinator.data.get("scene_playlists", []) if coordinator.data else []
)
for playlist in scene_playlists:
playlist_identifier = (DOMAIN, playlist["id"])
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={playlist_identifier},
name=playlist.get("name", playlist["id"]),
manufacturer=server_name,
model="Scene Playlist",
configuration_url=server_url,
)
current_identifiers.add(playlist_identifier)
# One device per sync clock — groups its switch/number/button/sensor.
sync_clocks = coordinator.data.get("sync_clocks", []) if coordinator.data else []
for clock in sync_clocks:
@@ -140,37 +156,45 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
DATA_EVENT_LISTENER: event_listener,
}
# Track target, scene, and sync-clock IDs to detect changes
# Track target, scene, playlist, and sync-clock IDs to detect changes
known_target_ids = set(coordinator.data.get("targets", {}).keys() if coordinator.data else [])
known_scene_ids = set(
p["id"] for p in (coordinator.data.get("scene_presets", []) if coordinator.data else [])
)
known_playlist_ids = set(
p["id"] for p in (coordinator.data.get("scene_playlists", []) if coordinator.data else [])
)
known_clock_ids = set(
c["id"] for c in (coordinator.data.get("sync_clocks", []) if coordinator.data else [])
)
def _on_coordinator_update() -> None:
"""Detect target/scene/clock list changes and trigger reload."""
nonlocal known_target_ids, known_scene_ids, known_clock_ids
"""Detect target/scene/playlist/clock list changes and trigger reload."""
nonlocal known_target_ids, known_scene_ids, known_playlist_ids, known_clock_ids
if not coordinator.data:
return
targets = coordinator.data.get("targets", {})
# Reload if target, scene, or sync-clock list changed
# Reload if target, scene, playlist, or sync-clock list changed
current_ids = set(targets.keys())
current_scene_ids = set(p["id"] for p in coordinator.data.get("scene_presets", []))
current_playlist_ids = set(p["id"] for p in coordinator.data.get("scene_playlists", []))
current_clock_ids = set(c["id"] for c in coordinator.data.get("sync_clocks", []))
if (
current_ids != known_target_ids
or current_scene_ids != known_scene_ids
or current_playlist_ids != known_playlist_ids
or current_clock_ids != known_clock_ids
):
known_target_ids = current_ids
known_scene_ids = current_scene_ids
known_playlist_ids = current_playlist_ids
known_clock_ids = current_clock_ids
_LOGGER.info("Target, scene, or sync-clock list changed, reloading integration")
_LOGGER.info(
"Target, scene, playlist, or sync-clock list changed, reloading integration"
)
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
coordinator.async_add_listener(_on_coordinator_update)