feat(scada): add calibrated monitoring and incident recovery
This commit is contained in:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user