cdba98813b
Performance (hot path): - Fix double brightness: removed duplicate scaling from 9 device clients (wled, adalight, ambiled, openrgb, hue, spi, chroma, gamesense, usbhid, espnow) — processor loop is now the single source of brightness - Bounded send_timestamps deque with maxlen, removed 3 cleanup loops - Running FPS sum O(1) instead of sum()/len() O(n) per frame - datetime.now(timezone.utc) → time.monotonic() with lazy conversion - Device info refresh interval 30 → 300 iterations - Composite: gate layer_snapshots copy on preview client flag - Composite: versioned sub_streams snapshot (copy only on change) - Composite: pre-resolved blend methods (dict lookup vs getattr) - ApiInput: np.copyto in-place instead of astype allocation Code quality: - BaseJsonStore: RLock on get/delete/get_all/count (was created but unused) - EntityNotFoundError → proper 404 responses across 15 route files - Remove 21 defensive getattr(x,'tags',[]) — field guaranteed on all models - Fix Dict[str,any] → Dict[str,Any] in template/audio_template stores - Log 4 silenced exceptions (automation engine, metrics, system) - ValueStream.get_value() now @abstractmethod - Config.from_yaml: add encoding="utf-8" - OutputTargetStore: remove 25-line _load override, use _legacy_json_keys - BaseJsonStore: add _legacy_json_keys for migration support - Remove unnecessary except Exception→500 from postprocessing list endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
205 lines
7.4 KiB
Python
205 lines
7.4 KiB
Python
"""Sync clock routes: CRUD + runtime control for synchronization clocks."""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
from wled_controller.api.auth import AuthRequired
|
|
from wled_controller.api.dependencies import (
|
|
fire_entity_event,
|
|
get_color_strip_store,
|
|
get_sync_clock_manager,
|
|
get_sync_clock_store,
|
|
)
|
|
from wled_controller.api.schemas.sync_clocks import (
|
|
SyncClockCreate,
|
|
SyncClockListResponse,
|
|
SyncClockResponse,
|
|
SyncClockUpdate,
|
|
)
|
|
from wled_controller.storage.sync_clock import SyncClock
|
|
from wled_controller.storage.sync_clock_store import SyncClockStore
|
|
from wled_controller.storage.color_strip_store import ColorStripStore
|
|
from wled_controller.core.processing.sync_clock_manager import SyncClockManager
|
|
from wled_controller.utils import get_logger
|
|
from wled_controller.storage.base_store import EntityNotFoundError
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _to_response(clock: SyncClock, manager: SyncClockManager) -> SyncClockResponse:
|
|
"""Convert a SyncClock to a SyncClockResponse (with runtime state)."""
|
|
rt = manager.get_runtime(clock.id)
|
|
return SyncClockResponse(
|
|
id=clock.id,
|
|
name=clock.name,
|
|
speed=rt.speed if rt else clock.speed,
|
|
description=clock.description,
|
|
tags=clock.tags,
|
|
is_running=rt.is_running if rt else True,
|
|
elapsed_time=rt.get_time() if rt else 0.0,
|
|
created_at=clock.created_at,
|
|
updated_at=clock.updated_at,
|
|
)
|
|
|
|
|
|
@router.get("/api/v1/sync-clocks", response_model=SyncClockListResponse, tags=["Sync Clocks"])
|
|
async def list_sync_clocks(
|
|
_auth: AuthRequired,
|
|
store: SyncClockStore = Depends(get_sync_clock_store),
|
|
manager: SyncClockManager = Depends(get_sync_clock_manager),
|
|
):
|
|
"""List all synchronization clocks."""
|
|
clocks = store.get_all_clocks()
|
|
return SyncClockListResponse(
|
|
clocks=[_to_response(c, manager) for c in clocks],
|
|
count=len(clocks),
|
|
)
|
|
|
|
|
|
@router.post("/api/v1/sync-clocks", response_model=SyncClockResponse, status_code=201, tags=["Sync Clocks"])
|
|
async def create_sync_clock(
|
|
data: SyncClockCreate,
|
|
_auth: AuthRequired,
|
|
store: SyncClockStore = Depends(get_sync_clock_store),
|
|
manager: SyncClockManager = Depends(get_sync_clock_manager),
|
|
):
|
|
"""Create a new synchronization clock."""
|
|
try:
|
|
clock = store.create_clock(
|
|
name=data.name,
|
|
speed=data.speed,
|
|
description=data.description,
|
|
tags=data.tags,
|
|
)
|
|
fire_entity_event("sync_clock", "created", clock.id)
|
|
return _to_response(clock, manager)
|
|
except EntityNotFoundError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.get("/api/v1/sync-clocks/{clock_id}", response_model=SyncClockResponse, tags=["Sync Clocks"])
|
|
async def get_sync_clock(
|
|
clock_id: str,
|
|
_auth: AuthRequired,
|
|
store: SyncClockStore = Depends(get_sync_clock_store),
|
|
manager: SyncClockManager = Depends(get_sync_clock_manager),
|
|
):
|
|
"""Get a synchronization clock by ID."""
|
|
try:
|
|
clock = store.get_clock(clock_id)
|
|
return _to_response(clock, manager)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
|
|
|
|
@router.put("/api/v1/sync-clocks/{clock_id}", response_model=SyncClockResponse, tags=["Sync Clocks"])
|
|
async def update_sync_clock(
|
|
clock_id: str,
|
|
data: SyncClockUpdate,
|
|
_auth: AuthRequired,
|
|
store: SyncClockStore = Depends(get_sync_clock_store),
|
|
manager: SyncClockManager = Depends(get_sync_clock_manager),
|
|
):
|
|
"""Update a synchronization clock. Speed changes are hot-applied to running streams."""
|
|
try:
|
|
clock = store.update_clock(
|
|
clock_id=clock_id,
|
|
name=data.name,
|
|
speed=data.speed,
|
|
description=data.description,
|
|
tags=data.tags,
|
|
)
|
|
# Hot-update runtime speed
|
|
if data.speed is not None:
|
|
manager.update_speed(clock_id, clock.speed)
|
|
fire_entity_event("sync_clock", "updated", clock_id)
|
|
return _to_response(clock, manager)
|
|
except EntityNotFoundError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.delete("/api/v1/sync-clocks/{clock_id}", status_code=204, tags=["Sync Clocks"])
|
|
async def delete_sync_clock(
|
|
clock_id: str,
|
|
_auth: AuthRequired,
|
|
store: SyncClockStore = Depends(get_sync_clock_store),
|
|
css_store: ColorStripStore = Depends(get_color_strip_store),
|
|
manager: SyncClockManager = Depends(get_sync_clock_manager),
|
|
):
|
|
"""Delete a synchronization clock (fails if referenced by CSS sources)."""
|
|
try:
|
|
# Check references
|
|
for source in css_store.get_all_sources():
|
|
if getattr(source, "clock_id", None) == clock_id:
|
|
raise ValueError(
|
|
f"Cannot delete: referenced by color strip source '{source.name}'"
|
|
)
|
|
manager.release_all_for(clock_id)
|
|
store.delete_clock(clock_id)
|
|
fire_entity_event("sync_clock", "deleted", clock_id)
|
|
except EntityNotFoundError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
# ── Runtime control ──────────────────────────────────────────────────
|
|
|
|
@router.post("/api/v1/sync-clocks/{clock_id}/pause", response_model=SyncClockResponse, tags=["Sync Clocks"])
|
|
async def pause_sync_clock(
|
|
clock_id: str,
|
|
_auth: AuthRequired,
|
|
store: SyncClockStore = Depends(get_sync_clock_store),
|
|
manager: SyncClockManager = Depends(get_sync_clock_manager),
|
|
):
|
|
"""Pause a synchronization clock — all linked animations freeze."""
|
|
try:
|
|
clock = store.get_clock(clock_id)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
manager.pause(clock_id)
|
|
fire_entity_event("sync_clock", "updated", clock_id)
|
|
return _to_response(clock, manager)
|
|
|
|
|
|
@router.post("/api/v1/sync-clocks/{clock_id}/resume", response_model=SyncClockResponse, tags=["Sync Clocks"])
|
|
async def resume_sync_clock(
|
|
clock_id: str,
|
|
_auth: AuthRequired,
|
|
store: SyncClockStore = Depends(get_sync_clock_store),
|
|
manager: SyncClockManager = Depends(get_sync_clock_manager),
|
|
):
|
|
"""Resume a paused synchronization clock."""
|
|
try:
|
|
clock = store.get_clock(clock_id)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
manager.resume(clock_id)
|
|
fire_entity_event("sync_clock", "updated", clock_id)
|
|
return _to_response(clock, manager)
|
|
|
|
|
|
@router.post("/api/v1/sync-clocks/{clock_id}/reset", response_model=SyncClockResponse, tags=["Sync Clocks"])
|
|
async def reset_sync_clock(
|
|
clock_id: str,
|
|
_auth: AuthRequired,
|
|
store: SyncClockStore = Depends(get_sync_clock_store),
|
|
manager: SyncClockManager = Depends(get_sync_clock_manager),
|
|
):
|
|
"""Reset a synchronization clock to t=0 — all linked animations restart."""
|
|
try:
|
|
clock = store.get_clock(clock_id)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
manager.reset(clock_id)
|
|
fire_entity_event("sync_clock", "updated", clock_id)
|
|
return _to_response(clock, manager)
|