67 lines
2.9 KiB
Python
67 lines
2.9 KiB
Python
"""Read-only chronological SCADA paging shared by REST and replay processing."""
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import math
|
|
from datetime import datetime
|
|
|
|
from app.domain.schemas.scada import ScadaRawPage, ScadaRawReading
|
|
from app.infra.db.timescaledb.repositories.scada import ScadaRepository
|
|
|
|
|
|
def validate_window(start: datetime, end: datetime) -> None:
|
|
if start.tzinfo is None or end.tzinfo is None or start >= end:
|
|
raise ValueError("需要带时区、结束晚于开始的 SCADA 时间区间")
|
|
|
|
|
|
def raw_reading(row: dict) -> ScadaRawReading:
|
|
value = row["monitored_value"]
|
|
invalid = value is not None and not math.isfinite(float(value))
|
|
return ScadaRawReading(
|
|
time=row["time"], device_id=row["device_id"],
|
|
monitored_value=None if invalid else value,
|
|
quality="invalid" if invalid else "missing" if value is None else "unvalidated",
|
|
)
|
|
|
|
|
|
async def read_raw_page(
|
|
conn, *, project_id: str, device_ids: list[str], start: datetime,
|
|
end: datetime, limit: int = 500, cursor: str | None = None,
|
|
through: datetime | None = None,
|
|
) -> ScadaRawPage:
|
|
validate_window(start, end)
|
|
ids = sorted(set(device_ids))
|
|
if not ids or len(ids) > 500 or not 1 <= limit <= 500:
|
|
raise ValueError("请选择 1 至 500 个设备,每页最多 500 条")
|
|
cap = min(through or end, end)
|
|
if cap.tzinfo is None or cap < start:
|
|
raise ValueError("模拟时间不在可读取区间内")
|
|
signature = hashlib.sha256(json.dumps(
|
|
[project_id, ids, start.isoformat(), end.isoformat()], separators=(",", ":")
|
|
).encode()).hexdigest()
|
|
after = None
|
|
if cursor:
|
|
try:
|
|
data = json.loads(base64.urlsafe_b64decode(cursor.encode()))
|
|
frozen = datetime.fromisoformat(data["through"])
|
|
last = datetime.fromisoformat(data["time"])
|
|
if (data["query"] != signature or frozen.tzinfo is None or last.tzinfo is None
|
|
or frozen > cap or not start <= last <= frozen or data["device"] not in ids):
|
|
raise ValueError()
|
|
cap, after = frozen, (last, data["device"])
|
|
except (ValueError, KeyError, TypeError, UnicodeError) as exc:
|
|
raise ValueError("SCADA 分页游标无效或查询范围已改变") from exc
|
|
rows = await ScadaRepository.get_raw_page(conn, ids, start, end, cap, limit + 1, after)
|
|
selected = rows[:limit]
|
|
next_cursor = None
|
|
if len(rows) > limit:
|
|
last = selected[-1]
|
|
next_cursor = base64.urlsafe_b64encode(json.dumps({
|
|
"query": signature, "through": cap.isoformat(),
|
|
"time": last["time"].isoformat(), "device": last["device_id"],
|
|
}, separators=(",", ":")).encode()).decode()
|
|
return ScadaRawPage(items=[raw_reading(row) for row in selected],
|
|
next_cursor=next_cursor, scanned_through=cap)
|