feat(snapshot): include scene playlists + cycling state in snapshot
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.
This commit is contained in:
@@ -30,7 +30,9 @@ from ledgrab.api.dependencies import (
|
|||||||
get_color_strip_store,
|
get_color_strip_store,
|
||||||
get_device_store,
|
get_device_store,
|
||||||
get_output_target_store,
|
get_output_target_store,
|
||||||
|
get_playlist_engine,
|
||||||
get_processor_manager,
|
get_processor_manager,
|
||||||
|
get_scene_playlist_store,
|
||||||
get_scene_preset_store,
|
get_scene_preset_store,
|
||||||
get_sync_clock_manager,
|
get_sync_clock_manager,
|
||||||
get_sync_clock_store,
|
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 .color_strip_sources.crud import list_color_strip_sources
|
||||||
from .devices import list_devices, resolve_device_brightness
|
from .devices import list_devices, resolve_device_brightness
|
||||||
from .output_targets import batch_target_metrics, batch_target_states, list_targets
|
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 .scene_presets import list_scene_presets
|
||||||
from .sync_clocks import list_sync_clocks
|
from .sync_clocks import list_sync_clocks
|
||||||
from .system import get_system_performance, health_check
|
from .system import get_system_performance, health_check
|
||||||
@@ -53,7 +56,9 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
router = APIRouter()
|
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 = (
|
SNAPSHOT_SECTIONS = (
|
||||||
"targets",
|
"targets",
|
||||||
"target_states",
|
"target_states",
|
||||||
@@ -63,6 +68,7 @@ SNAPSHOT_SECTIONS = (
|
|||||||
"css_sources",
|
"css_sources",
|
||||||
"value_sources",
|
"value_sources",
|
||||||
"scene_presets",
|
"scene_presets",
|
||||||
|
"scene_playlists",
|
||||||
"sync_clocks",
|
"sync_clocks",
|
||||||
"system",
|
"system",
|
||||||
)
|
)
|
||||||
@@ -135,6 +141,8 @@ async def get_snapshot(
|
|||||||
css_store=Depends(get_color_strip_store),
|
css_store=Depends(get_color_strip_store),
|
||||||
value_store=Depends(get_value_source_store),
|
value_store=Depends(get_value_source_store),
|
||||||
preset_store=Depends(get_scene_preset_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_store=Depends(get_sync_clock_store),
|
||||||
clock_manager=Depends(get_sync_clock_manager),
|
clock_manager=Depends(get_sync_clock_manager),
|
||||||
update_service=Depends(get_update_service),
|
update_service=Depends(get_update_service),
|
||||||
@@ -152,6 +160,8 @@ async def get_snapshot(
|
|||||||
"css_sources": [...],
|
"css_sources": [...],
|
||||||
"value_sources": [...],
|
"value_sources": [...],
|
||||||
"scene_presets": [...],
|
"scene_presets": [...],
|
||||||
|
"scene_playlists": [...],
|
||||||
|
"playlist_state": {...}, # companion to scene_playlists
|
||||||
"sync_clocks": [...],
|
"sync_clocks": [...],
|
||||||
"system": {"performance": {...}, "health": {...}, "update": {...}}
|
"system": {"performance": {...}, "health": {...}, "update": {...}}
|
||||||
}
|
}
|
||||||
@@ -184,6 +194,14 @@ async def get_snapshot(
|
|||||||
result["value_sources"] = (await list_value_sources(_auth, None, value_store)).sources
|
result["value_sources"] = (await list_value_sources(_auth, None, value_store)).sources
|
||||||
if "scene_presets" in sections:
|
if "scene_presets" in sections:
|
||||||
result["scene_presets"] = (await list_scene_presets(_auth, preset_store)).presets
|
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:
|
if "sync_clocks" in sections:
|
||||||
clocks = await list_sync_clocks(_auth, clock_store, clock_manager)
|
clocks = await list_sync_clocks(_auth, clock_store, clock_manager)
|
||||||
result["sync_clocks"] = clocks.clocks
|
result["sync_clocks"] = clocks.clocks
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ _TOP_LEVEL_KEYS = (
|
|||||||
"css_sources",
|
"css_sources",
|
||||||
"value_sources",
|
"value_sources",
|
||||||
"scene_presets",
|
"scene_presets",
|
||||||
|
"scene_playlists",
|
||||||
|
"playlist_state",
|
||||||
"sync_clocks",
|
"sync_clocks",
|
||||||
"system",
|
"system",
|
||||||
)
|
)
|
||||||
@@ -56,6 +58,21 @@ def client(test_config, monkeypatch):
|
|||||||
value_store.get_all_sources.return_value = []
|
value_store.get_all_sources.return_value = []
|
||||||
preset_store = MagicMock()
|
preset_store = MagicMock()
|
||||||
preset_store.get_all_presets.return_value = []
|
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 = MagicMock()
|
||||||
clock_store.get_all_clocks.return_value = []
|
clock_store.get_all_clocks.return_value = []
|
||||||
clock_manager = MagicMock()
|
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_color_strip_store] = lambda: css_store
|
||||||
app.dependency_overrides[deps.get_value_source_store] = lambda: value_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_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_store] = lambda: clock_store
|
||||||
app.dependency_overrides[deps.get_sync_clock_manager] = lambda: clock_manager
|
app.dependency_overrides[deps.get_sync_clock_manager] = lambda: clock_manager
|
||||||
app.dependency_overrides[deps.get_processor_manager] = lambda: manager
|
app.dependency_overrides[deps.get_processor_manager] = lambda: manager
|
||||||
@@ -97,12 +116,16 @@ def test_snapshot_returns_all_sections(client):
|
|||||||
"css_sources",
|
"css_sources",
|
||||||
"value_sources",
|
"value_sources",
|
||||||
"scene_presets",
|
"scene_presets",
|
||||||
|
"scene_playlists",
|
||||||
"sync_clocks",
|
"sync_clocks",
|
||||||
):
|
):
|
||||||
assert data[list_key] == []
|
assert data[list_key] == []
|
||||||
for dict_key in ("target_states", "target_metrics", "device_brightness"):
|
for dict_key in ("target_states", "target_metrics", "device_brightness"):
|
||||||
assert data[dict_key] == {}
|
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):
|
def test_snapshot_system_block_has_health_version(client):
|
||||||
data = client.get("/api/v1/snapshot", headers=_AUTH).json()
|
data = client.get("/api/v1/snapshot", headers=_AUTH).json()
|
||||||
|
|||||||
Reference in New Issue
Block a user