feat(scada): add project monitoring replay
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from app.domain.schemas.scada_replay import ScadaReplayCreate
|
||||
from app.services import scada_replay_monitoring
|
||||
|
||||
|
||||
TARGET = datetime(2026, 10, 16, tzinfo=timezone.utc)
|
||||
PRESSURE_IDS = [f"P{index}" for index in range(1, 6)]
|
||||
FLOW_IDS = ["F1"]
|
||||
|
||||
|
||||
def _document():
|
||||
config = ScadaReplayCreate.model_validate({
|
||||
"request_id": str(uuid4()),
|
||||
"start_time": TARGET - timedelta(days=15),
|
||||
"end_time": TARGET + timedelta(hours=1),
|
||||
"device_ids": PRESSURE_IDS + FLOW_IDS,
|
||||
"rules": [
|
||||
{"rule_id": "pressure_drop", "device_ids": PRESSURE_IDS,
|
||||
"kind": "relative_change", "direction": "decrease", "threshold": 20, "unit": "m"},
|
||||
{"rule_id": "outlet_flow", "device_ids": FLOW_IDS,
|
||||
"kind": "relative_change", "direction": "increase", "threshold": 20, "unit": "m3/h"},
|
||||
],
|
||||
})
|
||||
return {
|
||||
"replay_id": str(uuid4()),
|
||||
"config": config.model_dump(mode="json"),
|
||||
"monitoring_interval_seconds": 900,
|
||||
"monitoring_config": {
|
||||
"rule_version": "burst-monitor-v1",
|
||||
"pressure_device_ids": PRESSURE_IDS,
|
||||
"outlet_flow_device_ids": FLOW_IDS,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _rows():
|
||||
rows = []
|
||||
count = 15 * 96
|
||||
start = TARGET - timedelta(minutes=15 * (count - 1))
|
||||
for index in range(count):
|
||||
at = start + timedelta(minutes=15 * index)
|
||||
for device_id in PRESSURE_IDS:
|
||||
rows.append({"time": at.isoformat(), "device_id": device_id,
|
||||
"cleaned_value": 70 if at == TARGET else 100,
|
||||
"quality": "valid"})
|
||||
rows.append({"time": at.isoformat(), "device_id": "F1",
|
||||
"cleaned_value": 150 if at == TARGET else 100,
|
||||
"quality": "valid"})
|
||||
return rows
|
||||
|
||||
|
||||
def test_multiple_indicators_create_suspected_burst_result(monkeypatch):
|
||||
monkeypatch.setattr(scada_replay_monitoring, "run_burst_detection", lambda **_: {
|
||||
"rows": [{"Score": -0.05}],
|
||||
"summary": {"latest_sensor_rankings": []},
|
||||
})
|
||||
|
||||
result = scada_replay_monitoring.evaluate_monitoring_period(
|
||||
_document(), _rows(),
|
||||
period_start=TARGET - timedelta(minutes=15),
|
||||
period_end=TARGET,
|
||||
batch_sequence=1440,
|
||||
)
|
||||
|
||||
assert result["status"] == "suspected_burst"
|
||||
assert result["pressure"]["hit_device_ids"] == PRESSURE_IDS
|
||||
assert result["outlet_flow"]["hit_device_ids"] == FLOW_IDS
|
||||
assert result["burst_detection"]["hit"] is True
|
||||
|
||||
|
||||
def test_single_indicator_stays_attention(monkeypatch):
|
||||
monkeypatch.setattr(scada_replay_monitoring, "run_burst_detection", lambda **_: {
|
||||
"rows": [{"Score": 0.1}],
|
||||
"summary": {"latest_sensor_rankings": []},
|
||||
})
|
||||
rows = _rows()
|
||||
for row in rows:
|
||||
if row["device_id"] == "F1" and row["time"] == TARGET.isoformat():
|
||||
row["cleaned_value"] = 100
|
||||
|
||||
result = scada_replay_monitoring.evaluate_monitoring_period(
|
||||
_document(), rows,
|
||||
period_start=TARGET - timedelta(minutes=15),
|
||||
period_end=TARGET,
|
||||
batch_sequence=1440,
|
||||
)
|
||||
|
||||
assert result["status"] == "attention"
|
||||
assert result["pressure"]["hit"] is True
|
||||
assert result["outlet_flow"]["hit"] is False
|
||||
assert result["burst_detection"]["hit"] is False
|
||||
|
||||
|
||||
def test_missing_project_roles_is_data_gap_not_normal():
|
||||
document = _document()
|
||||
document["monitoring_config"] = None
|
||||
|
||||
result = scada_replay_monitoring.evaluate_monitoring_period(
|
||||
document, [],
|
||||
period_start=TARGET - timedelta(minutes=15),
|
||||
period_end=TARGET,
|
||||
batch_sequence=1,
|
||||
)
|
||||
|
||||
assert result["status"] == "insufficient_data"
|
||||
assert result["burst_detection"]["status"] == "not_run"
|
||||
Reference in New Issue
Block a user