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:
@@ -29,6 +29,7 @@ from .const import (
|
||||
DATA_COORDINATOR,
|
||||
)
|
||||
from .coordinator import LedGrabCoordinator
|
||||
from .entity import LedGrabPlaylistEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -59,6 +60,14 @@ async def async_setup_entry(
|
||||
for clock in coordinator.data.get("sync_clocks", []):
|
||||
entities.append(SyncClockElapsedSensor(coordinator, clock["id"], entry.entry_id))
|
||||
|
||||
for playlist in coordinator.data.get("scene_playlists", []):
|
||||
entities.append(
|
||||
PlaylistCurrentSceneSensor(coordinator, playlist["id"], entry.entry_id)
|
||||
)
|
||||
entities.append(
|
||||
PlaylistItemsSensor(coordinator, playlist["id"], entry.entry_id)
|
||||
)
|
||||
|
||||
entities.extend(_build_server_sensors(coordinator, entry.entry_id))
|
||||
|
||||
async_add_entities(entities)
|
||||
@@ -268,6 +277,93 @@ class SyncClockElapsedSensor(CoordinatorEntity, SensorEntity):
|
||||
return None
|
||||
|
||||
|
||||
class _PlaylistSensorBase(LedGrabPlaylistEntity, SensorEntity):
|
||||
"""Sensor attached to a scene-playlist device.
|
||||
|
||||
Device/availability/lookup plumbing lives in :class:`LedGrabPlaylistEntity`
|
||||
so it stays in sync with the playlist switch.
|
||||
"""
|
||||
|
||||
|
||||
class PlaylistCurrentSceneSensor(_PlaylistSensorBase):
|
||||
"""Name of the scene preset this playlist is currently holding.
|
||||
|
||||
Only this playlist's value populates while it is the active (cycling) one;
|
||||
every other playlist's sensor reads ``None`` (idle). The value advances as
|
||||
the engine steps to the next item.
|
||||
"""
|
||||
|
||||
_attr_icon = "mdi:playlist-star"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LedGrabCoordinator,
|
||||
playlist_id: str,
|
||||
entry_id: str,
|
||||
) -> None:
|
||||
super().__init__(coordinator, playlist_id, entry_id)
|
||||
self._attr_unique_id = f"{playlist_id}_current_scene"
|
||||
self._attr_translation_key = "playlist_current_scene"
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
state = self._running_state()
|
||||
if not state:
|
||||
return None
|
||||
preset_id = state.get("current_preset_id")
|
||||
if not preset_id:
|
||||
return None
|
||||
return self._resolve_preset_name(preset_id)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
state = self._running_state()
|
||||
if not state:
|
||||
return {}
|
||||
return {
|
||||
"current_preset_id": state.get("current_preset_id"),
|
||||
"current_index": state.get("current_index"),
|
||||
"item_count": state.get("item_count"),
|
||||
}
|
||||
|
||||
def _resolve_preset_name(self, preset_id: str) -> str:
|
||||
"""Map a scene-preset id to its display name (fall back to the id)."""
|
||||
if self.coordinator.data:
|
||||
for preset in self.coordinator.data.get("scene_presets", []):
|
||||
if preset.get("id") == preset_id:
|
||||
return preset.get("name", preset_id)
|
||||
return preset_id
|
||||
|
||||
|
||||
class PlaylistItemsSensor(_PlaylistSensorBase):
|
||||
"""Number of scene presets queued in a playlist (static config stat).
|
||||
|
||||
No ``state_class`` — this is a configuration cardinality that only changes
|
||||
when the playlist is edited, so it shouldn't be enrolled in long-term
|
||||
statistics.
|
||||
"""
|
||||
|
||||
_attr_icon = "mdi:playlist-music"
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LedGrabCoordinator,
|
||||
playlist_id: str,
|
||||
entry_id: str,
|
||||
) -> None:
|
||||
super().__init__(coordinator, playlist_id, entry_id)
|
||||
self._attr_unique_id = f"{playlist_id}_items"
|
||||
self._attr_translation_key = "playlist_items"
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
playlist = self._get_playlist()
|
||||
if playlist is None:
|
||||
return None
|
||||
return len(playlist.get("items") or [])
|
||||
|
||||
|
||||
class HALightMappedLightsSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Sensor showing the number of mapped HA lights for an HA Light target."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user