feat(scada): add project monitoring replay
This commit is contained in:
@@ -9,6 +9,86 @@ class ScadaRepository:
|
||||
VALUE_FIELDS = frozenset({"monitored_value", "cleaned_value"})
|
||||
RESULT_COLUMNS = "time, device_id, monitored_value, cleaned_value"
|
||||
|
||||
@staticmethod
|
||||
async def get_warmup_values(
|
||||
conn: AsyncConnection,
|
||||
device_ids: list[str],
|
||||
before: datetime,
|
||||
per_device_limit: int,
|
||||
) -> list[dict]:
|
||||
"""Return the latest raw values before a replay starts for detector warmup."""
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(
|
||||
"SELECT d.device_id,h.monitored_value FROM unnest(%s::text[]) d(device_id) "
|
||||
"CROSS JOIN LATERAL (SELECT monitored_value,time FROM scada.measurements "
|
||||
"WHERE device_id=d.device_id AND time<%s ORDER BY time DESC LIMIT %s) h "
|
||||
"ORDER BY d.device_id,h.time",
|
||||
(device_ids, before, per_device_limit),
|
||||
)
|
||||
return [dict(row) for row in await cur.fetchall()]
|
||||
|
||||
@staticmethod
|
||||
async def get_raw_page(
|
||||
conn: AsyncConnection, device_ids: list[str], start: datetime,
|
||||
end: datetime, through: datetime, limit: int,
|
||||
after: tuple[datetime, str] | None = None, *, frame: bool = False,
|
||||
) -> list[dict]:
|
||||
"""Read raw only, with a stable composite key and a bounded frame/page."""
|
||||
predicate = "device_id=ANY(%s) AND time>=%s AND time<%s AND time<=%s"
|
||||
params: tuple = (device_ids, start, end, through)
|
||||
if after is not None:
|
||||
predicate += " AND (time,device_id)>(%s,%s)"
|
||||
params += after
|
||||
if frame:
|
||||
statement = (
|
||||
"WITH next_time AS (SELECT min(time) AS time FROM scada.measurements WHERE "
|
||||
+ predicate + ") SELECT m.time,m.device_id,m.monitored_value,"
|
||||
"md5(COALESCE(m.monitored_value::text,'NULL')) AS source_hash "
|
||||
"FROM scada.measurements m WHERE m.device_id=ANY(%s) "
|
||||
"AND m.time=(SELECT time FROM next_time) ORDER BY m.time,m.device_id LIMIT %s"
|
||||
)
|
||||
params += (device_ids, limit)
|
||||
else:
|
||||
statement = (
|
||||
"SELECT time,device_id,monitored_value,"
|
||||
"md5(COALESCE(monitored_value::text,'NULL')) AS source_hash "
|
||||
"FROM scada.measurements WHERE " + predicate
|
||||
+ " ORDER BY time,device_id LIMIT %s"
|
||||
)
|
||||
params += (limit,)
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(statement, params)
|
||||
return [dict(row) for row in await cur.fetchall()]
|
||||
|
||||
@staticmethod
|
||||
async def apply_replay_cleaning(conn: AsyncConnection, rows: list[dict]) -> None:
|
||||
"""Guard every raw version; update corrected values without inserting raw."""
|
||||
if not rows:
|
||||
return
|
||||
async with conn.transaction():
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(
|
||||
"SELECT m.time,m.device_id,md5(COALESCE(m.monitored_value::text,'NULL')) AS source_hash "
|
||||
"FROM scada.measurements m JOIN unnest(%s::timestamptz[],%s::text[]) "
|
||||
"b(time,device_id) USING(time,device_id) FOR UPDATE OF m",
|
||||
([r["time"] for r in rows], [r["device_id"] for r in rows]),
|
||||
)
|
||||
existing = {(r["time"].isoformat(), r["device_id"]): r["source_hash"]
|
||||
for r in await cur.fetchall()}
|
||||
if len(existing) != len(rows) or any(
|
||||
existing.get((datetime.fromisoformat(r["time"]).isoformat(), r["device_id"]))
|
||||
!= r["source_hash"] for r in rows
|
||||
):
|
||||
raise ValueError("SOURCE_CHANGED: SCADA 原始值在处理期间发生变化")
|
||||
await cur.execute(
|
||||
"UPDATE scada.measurements m SET cleaned_value=b.value "
|
||||
"FROM unnest(%s::timestamptz[],%s::text[],%s::double precision[]) "
|
||||
"b(time,device_id,value) WHERE m.time=b.time AND m.device_id=b.device_id "
|
||||
"AND m.cleaned_value IS DISTINCT FROM b.value",
|
||||
([r["time"] for r in rows], [r["device_id"] for r in rows],
|
||||
[r["cleaned_value"] for r in rows]),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def insert_scada_batch(conn: AsyncConnection, data: List[dict]):
|
||||
if not data:
|
||||
|
||||
Reference in New Issue
Block a user