feat(scada): add calibrated monitoring and incident recovery

This commit is contained in:
2026-11-07 19:44:58 +08:00
parent 26fa5d1909
commit ff8aba4964
109 changed files with 9165 additions and 629 deletions
+27 -1
View File
@@ -3,12 +3,28 @@ from __future__ import annotations
import json
import math
import re
from datetime import datetime
from pathlib import Path
from typing import Annotated, Any, Literal
from jsonschema import Draft7Validator
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_serializer, model_validator
CHART_SAMPLE_TIME_PATTERN = (
r"^(?:[1-9][0-9]{3}|0[1-9][0-9]{2}|00[1-9][0-9]|000[1-9])-[0-9]{2}-[0-9]{2}T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]"
r"(?:\.[0-9]+)?(?:Z|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])$"
)
def validate_chart_sample_time(value: str) -> None:
"""Match the bounded frontend RFC3339 spelling and an actual calendar date."""
if len(value) > 64 or re.fullmatch(CHART_SAMPLE_TIME_PATTERN, value) is None:
raise ValueError("Chart sample times require RFC3339 seconds and a timezone")
if datetime.fromisoformat(value).tzinfo is None:
raise ValueError("Chart sample times require a timezone")
_catalog = json.loads(
(Path(__file__).resolve().parents[3] / "contracts/answer-ui-catalog.json").read_text()
)
@@ -71,8 +87,18 @@ class AnswerUiBlock(UiModel):
if element.type == "Chart":
if any(len(series["data"]) != len(props["x_data"]) for series in props["series"]):
raise ValueError("Chart series must match the axis")
if any(not math.isfinite(value) for series in props["series"] for value in series["data"]):
if any(value is not None and not math.isfinite(value) for series in props["series"] for value in series["data"]):
raise ValueError("Chart values must be finite")
for series in props["series"]:
times = series.get("sample_times")
if times is not None:
if len(times) != len(series["data"]):
raise ValueError("Chart sample times must match the axis")
for value, at in zip(series["data"], times, strict=True):
if at is not None:
if value is None:
raise ValueError("Chart sample times require an observed value")
validate_chart_sample_time(at)
if element.type == "Table" and any(len(row) != len(props["columns"]) for row in props["rows"]):
raise ValueError("Table row width must match its columns")
if element.type == "FollowUpForm":