"""SCADA replay controls. Manual steps are processed by the command request.""" from uuid import UUID from datetime import datetime from fastapi import APIRouter, Depends, Query, HTTPException from app.auth.metadata_dependencies import get_metadata_repository from app.auth.permissions import SCADA_VIEW, SCADA_CLEAN, require_permission, permissions_for_context from app.auth.project_dependencies import ProjectContext, get_project_pg_connection, get_project_timescale_connection from app.domain.schemas.scada_replay import ( ScadaReplayCapabilities, ScadaReplayCreate, ScadaReplayCommand, ScadaReplaySnapshot, ScadaReplayEventsPage, ScadaReplayReadings, ) from app.infra.db.postgresql.scada_replays import ScadaReplayRepository from app.domain.schemas.project_settings import ProjectSettings from app.services.scada_replay import ScadaReplayService, snapshot router = APIRouter(prefix="/scada-replays", tags=["SCADA Replay"]) async def repository( ctx: ProjectContext = Depends(require_permission(SCADA_VIEW)), connection=Depends(get_project_pg_connection), ): return ScadaReplayRepository(connection, ctx.project_id) async def project_settings( ctx: ProjectContext = Depends(require_permission(SCADA_VIEW)), metadata_repo=Depends(get_metadata_repository), ) -> ProjectSettings: project = await metadata_repo.get_project_by_id(ctx.project_id) if project is None: raise HTTPException(404, "当前项目不存在") return ProjectSettings.model_validate(project.settings or {}) @router.get("/capabilities", response_model=ScadaReplayCapabilities) async def replay_capabilities(repo=Depends(repository), ctx=Depends(require_permission(SCADA_VIEW)), settings=Depends(project_settings)): available = await repo.available() current = await repo.current() if available else None monitoring = settings.scada_monitoring monitoring_ready = bool( monitoring and len(monitoring.pressure_device_ids) >= 5 and monitoring.outlet_flow_device_ids ) if monitoring_ready: monitoring_reason = None elif monitoring is None: monitoring_reason = "项目尚未配置压力与出厂边界流量监测角色" elif len(monitoring.pressure_device_ids) < 5: monitoring_reason = "爆管侦测至少需要 5 个压力监测设备" else: monitoring_reason = "项目尚未配置出厂边界流量设备" return ScadaReplayCapabilities( available=available, can_write=SCADA_CLEAN in permissions_for_context(ctx), reason=None if available else "需先部署 SCADA 回放存储结构", monitoring_interval_seconds=settings.scada_monitoring_interval_seconds, burst_monitoring_ready=monitoring_ready, burst_monitoring_reason=monitoring_reason, current=snapshot(current) if current else None, ) @router.post("", response_model=ScadaReplaySnapshot, status_code=201) async def create_replay(command: ScadaReplayCreate, repo=Depends(repository), ctx=Depends(require_permission(SCADA_CLEAN)), settings=Depends(project_settings)): return await ScadaReplayService(repo, ctx).create( command, monitoring_interval_seconds=settings.scada_monitoring_interval_seconds, monitoring_config=settings.scada_monitoring, ) @router.get("/{replay_id}", response_model=ScadaReplaySnapshot) async def get_replay(replay_id: UUID, repo=Depends(repository)): await ScadaReplayService(repo, None).ensure_storage() return snapshot(await repo.get(replay_id)) @router.post("/{replay_id}/commands", response_model=ScadaReplaySnapshot) async def replay_command(replay_id: UUID, command: ScadaReplayCommand, repo=Depends(repository), ctx=Depends(require_permission(SCADA_CLEAN)), timescale=Depends(get_project_timescale_connection)): service = ScadaReplayService(repo, ctx, timescale) result = await service.command(replay_id, command) if command.action in {"step", "retry"} and result.step_pending: return await service.process_pending_step(replay_id) return result @router.get("/{replay_id}/events", response_model=ScadaReplayEventsPage) async def replay_events(replay_id: UUID, offset: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=200), repo=Depends(repository)): await ScadaReplayService(repo, None).ensure_storage() await repo.get(replay_id) items = await repo.events(replay_id, offset, limit + 1) return ScadaReplayEventsPage(items=items[:limit], next_offset=offset + limit if len(items) > limit else None) @router.get("/{replay_id}/observations", response_model=ScadaReplayReadings) async def replay_observations(replay_id: UUID, through: datetime, repo=Depends(repository)): await ScadaReplayService(repo, None).ensure_storage() document = await repo.get(replay_id) if (through.tzinfo is None or not document.get("delivered_until") or through > datetime.fromisoformat(document["delivered_until"]) or through < datetime.fromisoformat(document["start_time"])): raise HTTPException(422, "回看仅允许本轮已读取的时间区间") return ScadaReplayReadings( items=await repo.latest_outputs(replay_id, through), through=through, )