from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body from typing import List from datetime import datetime from psycopg import AsyncConnection from pydantic import BaseModel, Field, field_validator from app.infra.db.postgresql.scada import ScadaInfoRepository from app.infra.db.timescaledb.repositories.scada import ScadaRepository from .dependencies import get_postgres_connection, get_timescale_connection router = APIRouter() SCADA_BATCH_MAX_ITEMS = 10_000 class ScadaReadingBatchItem(BaseModel): time: datetime device_id: str = Field(min_length=1) monitored_value: float | None = None cleaned_value: float | None = None @field_validator("device_id") @classmethod def normalize_device_id(cls, value: str) -> str: normalized = value.strip() if not normalized: raise ValueError("device_id must not be blank") return normalized @router.post("/timeseries/scada-readings/batches", status_code=201, summary="批量插入SCADA监测数据") async def insert_scada_data( data: List[ScadaReadingBatchItem] = Body( ..., min_length=1, max_length=SCADA_BATCH_MAX_ITEMS, description="SCADA设备监测数据列表", ), conn: AsyncConnection = Depends(get_timescale_connection), postgres_conn: AsyncConnection = Depends(get_postgres_connection), ): """ 批量插入SCADA监测数据 将多个设备的实时监测数据批量插入时间序列数据库。 Args: data: SCADA设备监测数据列表,每项包含device_id、时间戳和监测值等信息 Returns: 插入成功的记录数 """ rows = [item.model_dump() for item in data] requested_ids = list(dict.fromkeys(item["device_id"] for item in rows)) existing_ids = await ScadaInfoRepository.get_existing_device_ids( postgres_conn, requested_ids ) missing_ids = [ device_id for device_id in requested_ids if device_id not in existing_ids ] if missing_ids: raise HTTPException( status_code=422, detail=f"SCADA devices do not exist in BizDB: {', '.join(missing_ids)}", ) await ScadaRepository.insert_scada_batch(conn, rows) return {"message": f"Inserted {len(data)} records"} @router.get("/timeseries/scada-readings", summary="按设备ID和时间范围查询SCADA数据") async def get_scada_by_ids_time_range( start_time: datetime = Query(..., description="查询开始时间"), end_time: datetime = Query(..., description="查询结束时间"), device_ids: str = Query( ..., description="设备ID列表,逗号分隔,如 'device1,device2,device3'" ), conn: AsyncConnection = Depends(get_timescale_connection), ): """ 按设备ID和时间范围查询SCADA监测数据 查询多个设备在指定时间范围内的所有监测数据。 Args: start_time: 查询开始时间 end_time: 查询结束时间 device_ids: 设备ID列表,用逗号分隔 Returns: SCADA监测数据列表 """ device_ids_list = ( [id.strip() for id in device_ids.split(",") if id.strip()] if device_ids else [] ) return await ScadaRepository.get_scada_by_ids_time_range( conn, device_ids_list, start_time, end_time ) @router.get( "/timeseries/scada-readings/fields", summary="按设备ID、字段和时间范围查询SCADA数据" ) async def get_scada_field_by_ids_time_range( start_time: datetime = Query(..., description="查询开始时间"), end_time: datetime = Query(..., description="查询结束时间"), field: str = Query(..., description="要查询的字段名称"), device_ids: str = Query( ..., description="设备ID列表,逗号分隔,如 'device1,device2,device3'" ), conn: AsyncConnection = Depends(get_timescale_connection), ): """ 按设备ID、字段和时间范围查询特定SCADA数据 查询多个设备在指定时间范围内的特定字段监测数据。 Args: start_time: 查询开始时间 end_time: 查询结束时间 field: 字段名称 device_ids: 设备ID列表,用逗号分隔 Returns: SCADA字段数据列表 Raises: HTTPException: 当字段不存在或查询参数无效时返回400错误 """ try: device_ids_list = ( [id.strip() for id in device_ids.split(",") if id.strip()] if device_ids else [] ) return await ScadaRepository.get_scada_field_by_id_time_range( conn, device_ids_list, start_time, end_time, field ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.patch("/timeseries/scada-readings/{device_id}/field", summary="更新SCADA设备字段") async def update_scada_field( device_id: str = Path(..., description="设备ID"), time: datetime = Query(..., description="更新数据的时间戳"), field: str = Query(..., description="要更新的字段名称"), value: float = Query(..., description="更新的字段值"), conn: AsyncConnection = Depends(get_timescale_connection), ): """ 更新指定设备的字段值 更新SCADA设备在特定时间的某个字段监测数据。 Args: device_id: 设备ID time: 数据时间戳 field: 字段名称 value: 字段新值 Returns: 更新结果信息 Raises: HTTPException: 当字段不存在或更新失败时返回400错误 """ try: await ScadaRepository.update_scada_field(conn, time, device_id, field, value) return {"message": "Updated successfully"} except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.delete("/timeseries/scada-readings", summary="按设备ID和时间范围删除SCADA数据") async def delete_scada_data( device_id: str = Query(..., description="设备ID"), start_time: datetime = Query(..., description="删除开始时间"), end_time: datetime = Query(..., description="删除结束时间"), conn: AsyncConnection = Depends(get_timescale_connection), ): """ 删除指定设备和时间范围内的SCADA数据 删除在指定时间范围内的特定设备监测数据。 Args: device_id: 设备ID start_time: 删除开始时间 end_time: 删除结束时间 Returns: 删除结果信息 """ await ScadaRepository.delete_scada_by_id_time_range( conn, device_id, start_time, end_time ) return {"message": "Deleted successfully"}