From abc204c04e4ec18660689f015c4eb005091fa025 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Mon, 8 Jun 2026 16:22:47 +0300 Subject: [PATCH] feat(snapshot): include scene playlists + cycling state in snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The aggregated /api/v1/snapshot poll now emits a `scene_playlists` section (each playlist with its `is_running` flag) plus a companion `playlist_state` key carrying the single global cycling state (running playlist, current index/ preset, dwell) — so the HA-coordinator and other low-overhead pollers get playlist state in the same round trip as scenes/targets, matching the other entity sections. Gated by the `scene_playlists` include-section like the rest. Reuses the existing list_scene_playlists handler; snapshot route tests updated. --- server/src/ledgrab/api/routes/snapshot.py | 20 +++++++++++++++- .../tests/api/routes/test_snapshot_routes.py | 23 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/server/src/ledgrab/api/routes/snapshot.py b/server/src/ledgrab/api/routes/snapshot.py index 6b1d180..261e0d0 100644 --- a/server/src/ledgrab/api/routes/snapshot.py +++ b/server/src/ledgrab/api/routes/snapshot.py @@ -30,7 +30,9 @@ from ledgrab.api.dependencies import ( get_color_strip_store, get_device_store, get_output_target_store, + get_playlist_engine, get_processor_manager, + get_scene_playlist_store, get_scene_preset_store, get_sync_clock_manager, get_sync_clock_store, @@ -43,6 +45,7 @@ from ledgrab.utils import get_logger from .color_strip_sources.crud import list_color_strip_sources from .devices import list_devices, resolve_device_brightness from .output_targets import batch_target_metrics, batch_target_states, list_targets +from .scene_playlists import list_scene_playlists from .scene_presets import list_scene_presets from .sync_clocks import list_sync_clocks from .system import get_system_performance, health_check @@ -53,7 +56,9 @@ logger = get_logger(__name__) router = APIRouter() -# Selectable snapshot sections — these are exactly the response top-level keys. +# Selectable snapshot sections — these are exactly the response top-level keys, +# except ``scene_playlists`` which also emits a companion ``playlist_state`` key +# (the single global cycling state; see the handler). SNAPSHOT_SECTIONS = ( "targets", "target_states", @@ -63,6 +68,7 @@ SNAPSHOT_SECTIONS = ( "css_sources", "value_sources", "scene_presets", + "scene_playlists", "sync_clocks", "system", ) @@ -135,6 +141,8 @@ async def get_snapshot( css_store=Depends(get_color_strip_store), value_store=Depends(get_value_source_store), preset_store=Depends(get_scene_preset_store), + playlist_store=Depends(get_scene_playlist_store), + playlist_engine=Depends(get_playlist_engine), clock_store=Depends(get_sync_clock_store), clock_manager=Depends(get_sync_clock_manager), update_service=Depends(get_update_service), @@ -152,6 +160,8 @@ async def get_snapshot( "css_sources": [...], "value_sources": [...], "scene_presets": [...], + "scene_playlists": [...], + "playlist_state": {...}, # companion to scene_playlists "sync_clocks": [...], "system": {"performance": {...}, "health": {...}, "update": {...}} } @@ -184,6 +194,14 @@ async def get_snapshot( result["value_sources"] = (await list_value_sources(_auth, None, value_store)).sources if "scene_presets" in sections: result["scene_presets"] = (await list_scene_presets(_auth, preset_store)).presets + if "scene_playlists" in sections: + # One call returns both the playlist list (each with ``is_running``) and + # the single global cycling state (current index / preset / dwell). The + # state is emitted as a companion top-level key because it describes the + # one running playlist, not any individual list entry. + playlists = await list_scene_playlists(_auth, playlist_store, playlist_engine) + result["scene_playlists"] = playlists.playlists + result["playlist_state"] = playlists.state if "sync_clocks" in sections: clocks = await list_sync_clocks(_auth, clock_store, clock_manager) result["sync_clocks"] = clocks.clocks diff --git a/server/tests/api/routes/test_snapshot_routes.py b/server/tests/api/routes/test_snapshot_routes.py index a009bab..03fa8ae 100644 --- a/server/tests/api/routes/test_snapshot_routes.py +++ b/server/tests/api/routes/test_snapshot_routes.py @@ -33,6 +33,8 @@ _TOP_LEVEL_KEYS = ( "css_sources", "value_sources", "scene_presets", + "scene_playlists", + "playlist_state", "sync_clocks", "system", ) @@ -56,6 +58,21 @@ def client(test_config, monkeypatch): value_store.get_all_sources.return_value = [] preset_store = MagicMock() preset_store.get_all_presets.return_value = [] + playlist_store = MagicMock() + playlist_store.get_all_playlists.return_value = [] + playlist_engine = MagicMock() + playlist_engine.get_running_playlist_id.return_value = None + playlist_engine.get_state.return_value = { + "is_running": False, + "playlist_id": None, + "playlist_name": None, + "current_index": 0, + "item_count": 0, + "current_preset_id": None, + "started_at": None, + "step_started_at": None, + "step_duration": 0.0, + } clock_store = MagicMock() clock_store.get_all_clocks.return_value = [] clock_manager = MagicMock() @@ -74,6 +91,8 @@ def client(test_config, monkeypatch): app.dependency_overrides[deps.get_color_strip_store] = lambda: css_store app.dependency_overrides[deps.get_value_source_store] = lambda: value_store app.dependency_overrides[deps.get_scene_preset_store] = lambda: preset_store + app.dependency_overrides[deps.get_scene_playlist_store] = lambda: playlist_store + app.dependency_overrides[deps.get_playlist_engine] = lambda: playlist_engine app.dependency_overrides[deps.get_sync_clock_store] = lambda: clock_store app.dependency_overrides[deps.get_sync_clock_manager] = lambda: clock_manager app.dependency_overrides[deps.get_processor_manager] = lambda: manager @@ -97,12 +116,16 @@ def test_snapshot_returns_all_sections(client): "css_sources", "value_sources", "scene_presets", + "scene_playlists", "sync_clocks", ): assert data[list_key] == [] for dict_key in ("target_states", "target_metrics", "device_brightness"): assert data[dict_key] == {} + # The single global cycling state rides along with the playlist list. + assert data["playlist_state"]["is_running"] is False + def test_snapshot_system_block_has_health_version(client): data = client.get("/api/v1/snapshot", headers=_AUTH).json()