115 lines
5.2 KiB
Python
115 lines
5.2 KiB
Python
"""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,
|
|
)
|