189 lines
6.5 KiB
Python
189 lines
6.5 KiB
Python
from dataclasses import dataclass
|
|
from types import MappingProxyType
|
|
from typing import Any, Mapping
|
|
|
|
from psycopg import AsyncConnection
|
|
|
|
from app.native.wndb.core.database import read_all, try_read
|
|
|
|
|
|
_SCADA_VIEW_SELECT = """
|
|
SELECT id AS device_id, device_type, node_id, link_id, api_query_id,
|
|
measurement_unit, transmission_mode, transmission_frequency, reliability, x, y,
|
|
ST_X(ST_Transform(geom, 4326)) AS longitude,
|
|
ST_Y(ST_Transform(geom, 4326)) AS latitude
|
|
FROM gis.scada_devices
|
|
"""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ScadaElementMappings:
|
|
reservoirs: Mapping[str, str]
|
|
tanks: Mapping[str, str]
|
|
fixed_pumps: Mapping[str, str]
|
|
variable_pumps: Mapping[str, str]
|
|
pressure: Mapping[str, str]
|
|
demand: Mapping[str, str]
|
|
quality: Mapping[str, str]
|
|
|
|
|
|
def _empty_mapping_groups() -> dict[str, dict[str, str]]:
|
|
return {
|
|
"reservoir_liquid_level": {},
|
|
"tank_liquid_level": {},
|
|
"fixed_pump": {},
|
|
"variable_pump": {},
|
|
"pressure": {},
|
|
"demand": {},
|
|
"quality": {},
|
|
}
|
|
|
|
def _optional_text(value: Any) -> str | None:
|
|
return str(value).strip() if value is not None else None
|
|
|
|
|
|
def _optional_float(value: Any) -> float | None:
|
|
return float(value) if value is not None else None
|
|
|
|
|
|
def _optional_int(value: Any) -> int | None:
|
|
return int(value) if value is not None else None
|
|
|
|
|
|
def _device(record: dict[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"device_id": str(record["device_id"]).strip(),
|
|
"device_type": str(record["device_type"]).strip().lower(),
|
|
"node_id": _optional_text(record["node_id"]),
|
|
"link_id": _optional_text(record["link_id"]),
|
|
"api_query_id": _optional_text(record["api_query_id"]),
|
|
"measurement_unit": str(record["measurement_unit"]).strip(),
|
|
"transmission_mode": record["transmission_mode"],
|
|
"transmission_frequency": record["transmission_frequency"],
|
|
"reliability": _optional_int(record["reliability"]),
|
|
"x": _optional_float(record["x"]),
|
|
"y": _optional_float(record["y"]),
|
|
"longitude": _optional_float(record["longitude"]),
|
|
"latitude": _optional_float(record["latitude"]),
|
|
}
|
|
|
|
|
|
class ScadaInfoRepository:
|
|
"""Read SCADA metadata from the current project's business database."""
|
|
|
|
@staticmethod
|
|
async def get_scadas(conn: AsyncConnection) -> list[dict[str, Any]]:
|
|
async with conn.cursor() as cur:
|
|
await cur.execute(
|
|
_SCADA_VIEW_SELECT + " ORDER BY device_id"
|
|
)
|
|
records = await cur.fetchall()
|
|
|
|
return [_device(record) for record in records]
|
|
|
|
@staticmethod
|
|
async def get_scada(
|
|
conn: AsyncConnection, device_id: str
|
|
) -> dict[str, Any] | None:
|
|
async with conn.cursor() as cur:
|
|
await cur.execute(
|
|
_SCADA_VIEW_SELECT + " WHERE id = %s",
|
|
(device_id,),
|
|
)
|
|
record = await cur.fetchone()
|
|
return _device(record) if record else None
|
|
|
|
@staticmethod
|
|
async def get_existing_device_ids(
|
|
conn: AsyncConnection, device_ids: list[str]
|
|
) -> set[str]:
|
|
if not device_ids:
|
|
return set()
|
|
async with conn.cursor() as cur:
|
|
await cur.execute(
|
|
"SELECT device_id FROM asset.scada_devices WHERE device_id = ANY(%s)",
|
|
(device_ids,),
|
|
)
|
|
return {str(row["device_id"]).strip() for row in await cur.fetchall()}
|
|
|
|
@staticmethod
|
|
async def get_scadas_for_elements(
|
|
conn: AsyncConnection,
|
|
node_ids: list[str],
|
|
link_ids: list[str],
|
|
) -> list[dict[str, Any]]:
|
|
if not node_ids and not link_ids:
|
|
return []
|
|
async with conn.cursor() as cur:
|
|
await cur.execute(
|
|
_SCADA_VIEW_SELECT
|
|
+ " WHERE node_id = ANY(%s) OR link_id = ANY(%s) ORDER BY device_id",
|
|
(node_ids, link_ids),
|
|
)
|
|
return [_device(record) for record in await cur.fetchall()]
|
|
|
|
|
|
def get_scada_info_schema(name: str) -> dict[str, dict[str, Any]]:
|
|
return {
|
|
"device_id": {"type": "str", "optional": False, "readonly": True},
|
|
"device_type": {"type": "str", "optional": False, "readonly": True},
|
|
"node_id": {"type": "str", "optional": True, "readonly": True},
|
|
"link_id": {"type": "str", "optional": True, "readonly": True},
|
|
"api_query_id": {"type": "str", "optional": True, "readonly": True},
|
|
"measurement_unit": {"type": "str", "optional": False, "readonly": True},
|
|
"transmission_mode": {"type": "str", "optional": False, "readonly": True},
|
|
"transmission_frequency": {"type": "str", "optional": False, "readonly": True},
|
|
"reliability": {"type": "int", "optional": False, "readonly": True},
|
|
"x": {"type": "float", "optional": True, "readonly": True},
|
|
"y": {"type": "float", "optional": True, "readonly": True},
|
|
"longitude": {"type": "float", "optional": True, "readonly": True},
|
|
"latitude": {"type": "float", "optional": True, "readonly": True},
|
|
}
|
|
|
|
|
|
def get_scada_info(name: str, device_id: str) -> dict[str, Any]:
|
|
row = try_read(
|
|
name,
|
|
_SCADA_VIEW_SELECT + " WHERE id = %s",
|
|
(device_id,),
|
|
)
|
|
return _device(row) if row else {}
|
|
|
|
|
|
def get_all_scada_info(name: str) -> list[dict[str, Any]]:
|
|
return [
|
|
_device(row)
|
|
for row in read_all(name, _SCADA_VIEW_SELECT + " ORDER BY device_id")
|
|
]
|
|
|
|
|
|
def load_realtime_element_mappings(name: str) -> ScadaElementMappings:
|
|
"""Load one project-local immutable SCADA-to-model mapping snapshot."""
|
|
groups = _empty_mapping_groups()
|
|
rows = read_all(
|
|
name,
|
|
"""
|
|
SELECT device_type, COALESCE(node_id, link_id) AS element_id,
|
|
api_query_id
|
|
FROM asset.scada_devices
|
|
WHERE transmission_mode = 'realtime'
|
|
AND api_query_id IS NOT NULL
|
|
""",
|
|
)
|
|
for row in rows:
|
|
group = groups.get(str(row["device_type"]).strip().lower())
|
|
if group is not None:
|
|
group[str(row["element_id"]).strip()] = str(row["api_query_id"]).strip()
|
|
immutable = {
|
|
name: MappingProxyType(values.copy()) for name, values in groups.items()
|
|
}
|
|
return ScadaElementMappings(
|
|
reservoirs=immutable["reservoir_liquid_level"],
|
|
tanks=immutable["tank_liquid_level"],
|
|
fixed_pumps=immutable["fixed_pump"],
|
|
variable_pumps=immutable["variable_pump"],
|
|
pressure=immutable["pressure"],
|
|
demand=immutable["demand"],
|
|
quality=immutable["quality"],
|
|
)
|