feat(timeseries): unify element history queries
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
from typing import Literal
|
||||
|
||||
|
||||
Metric = Literal["flow", "pressure", "velocity"]
|
||||
|
||||
DISPLAY_UNITS: dict[Metric, str] = {
|
||||
"flow": "m³/h",
|
||||
"pressure": "m",
|
||||
"velocity": "m/s",
|
||||
}
|
||||
|
||||
def display_unit_for_metric(metric: Metric) -> str:
|
||||
return DISPLAY_UNITS[metric]
|
||||
@@ -9,6 +9,7 @@ class ScadaDeviceResponse(BaseModel):
|
||||
node_id: str | None = None
|
||||
link_id: str | None = None
|
||||
api_query_id: str | None = None
|
||||
measurement_unit: str
|
||||
transmission_mode: str
|
||||
transmission_frequency: str
|
||||
reliability: int | None = None
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
||||
|
||||
class HistoryElementType(StrEnum):
|
||||
PIPE = "pipe"
|
||||
JUNCTION = "junction"
|
||||
|
||||
|
||||
class HistoryMode(StrEnum):
|
||||
OBSERVED = "observed"
|
||||
REALTIME_COMPARISON = "realtime_comparison"
|
||||
ANALYSIS_COMPARISON = "analysis_comparison"
|
||||
|
||||
|
||||
class HistoryMetric(StrEnum):
|
||||
FLOW = "flow"
|
||||
PRESSURE = "pressure"
|
||||
|
||||
|
||||
class HistorySource(StrEnum):
|
||||
SCADA_RAW = "scada_raw"
|
||||
SCADA_CLEANED = "scada_cleaned"
|
||||
REALTIME_SIMULATION = "realtime_simulation"
|
||||
ANALYSIS_SIMULATION = "analysis_simulation"
|
||||
|
||||
|
||||
class ElementHistoryTarget(BaseModel):
|
||||
element_id: str = Field(min_length=1, max_length=128)
|
||||
element_type: HistoryElementType
|
||||
device_ids: list[str] | None = Field(default=None, max_length=100)
|
||||
|
||||
@field_validator("element_id")
|
||||
@classmethod
|
||||
def normalize_element_id(cls, value: str) -> str:
|
||||
return value.strip()
|
||||
|
||||
@field_validator("device_ids")
|
||||
@classmethod
|
||||
def normalize_device_ids(cls, value: list[str] | None) -> list[str] | None:
|
||||
if value is None:
|
||||
return None
|
||||
normalized = list(dict.fromkeys(item.strip() for item in value if item.strip()))
|
||||
if not normalized:
|
||||
raise ValueError("device_ids must contain at least one device")
|
||||
return normalized
|
||||
|
||||
|
||||
class ElementHistoryQuery(BaseModel):
|
||||
start_time: datetime
|
||||
end_time: datetime
|
||||
mode: HistoryMode = HistoryMode.OBSERVED
|
||||
run_id: UUID | None = None
|
||||
elements: list[ElementHistoryTarget] = Field(min_length=1, max_length=200)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_query(self) -> "ElementHistoryQuery":
|
||||
if self.start_time >= self.end_time:
|
||||
raise ValueError("start_time must be earlier than end_time")
|
||||
if self.mode == HistoryMode.ANALYSIS_COMPARISON and self.run_id is None:
|
||||
raise ValueError("run_id is required for analysis_comparison")
|
||||
if self.mode != HistoryMode.ANALYSIS_COMPARISON and self.run_id is not None:
|
||||
raise ValueError("run_id is only valid for analysis_comparison")
|
||||
return self
|
||||
|
||||
|
||||
class HistoryPoint(BaseModel):
|
||||
time: datetime
|
||||
value: float | None
|
||||
|
||||
|
||||
class ElementHistorySeries(BaseModel):
|
||||
element_id: str
|
||||
element_type: HistoryElementType
|
||||
device_id: str | None = None
|
||||
metric: HistoryMetric
|
||||
source: HistorySource
|
||||
source_unit: str = Field(description="Unit used by the returned point values")
|
||||
display_unit: str = Field(description="Recommended UI display unit")
|
||||
unit_inferred: bool = False
|
||||
points: list[HistoryPoint]
|
||||
|
||||
|
||||
class ElementHistoryResponse(BaseModel):
|
||||
series: list[ElementHistorySeries]
|
||||
Reference in New Issue
Block a user