"""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)