first commit
This commit is contained in:
commit
4b98219bf7
144 changed files with 31561 additions and 0 deletions
248
backend/api/routes/scenarios.py
Normal file
248
backend/api/routes/scenarios.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
"""
|
||||
Scenario control API — proxies trigger/reset commands to the MQTT broker
|
||||
so the frontend can fire simulator scenarios over HTTP.
|
||||
"""
|
||||
import json
|
||||
import asyncio
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import aiomqtt
|
||||
|
||||
from core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ── Scenario catalogue ───────────────────────────────────────────────────────
|
||||
# Mirrors the definitions in simulators/scenarios/runner.py and compound.py.
|
||||
# Kept here so the frontend has a single typed source of truth.
|
||||
|
||||
SCENARIOS = [
|
||||
# ── Compound (multi-bot, time-sequenced) ─────────────────────────────────
|
||||
{
|
||||
"name": "HOT_NIGHT",
|
||||
"label": "Hot Night",
|
||||
"description": "CRAC-01 compressor trips silently. The backup unit overworks itself. Rack temps climb, power draw rises, VESDA alert fires.",
|
||||
"duration": "~10 min",
|
||||
"compound": True,
|
||||
"default_target": None,
|
||||
"targets": [],
|
||||
},
|
||||
{
|
||||
"name": "GENERATOR_TEST_GONE_WRONG",
|
||||
"label": "Generator Test Gone Wrong",
|
||||
"description": "Planned ATS transfer to generator. Generator was low on fuel and faults after 15 min. UPS must carry full site load alone.",
|
||||
"duration": "~16 min",
|
||||
"compound": True,
|
||||
"default_target": None,
|
||||
"targets": [],
|
||||
},
|
||||
{
|
||||
"name": "SLOW_BURN",
|
||||
"label": "Slow Burn",
|
||||
"description": "A dirty filter nobody noticed. Airflow degrades for 30 min. Temps creep, humidity climbs, VESDA alerts, then CRAC trips on thermal protection.",
|
||||
"duration": "~30 min",
|
||||
"compound": True,
|
||||
"default_target": None,
|
||||
"targets": [],
|
||||
},
|
||||
{
|
||||
"name": "LAST_RESORT",
|
||||
"label": "Last Resort",
|
||||
"description": "Utility fails. Generator starts then faults after 2 minutes. UPS absorbs the full load, overheats, and VESDA escalates to fire.",
|
||||
"duration": "~9 min",
|
||||
"compound": True,
|
||||
"default_target": None,
|
||||
"targets": [],
|
||||
},
|
||||
# ── Cooling ──────────────────────────────────────────────────────────────
|
||||
{
|
||||
"name": "COOLING_FAILURE",
|
||||
"label": "Cooling Failure",
|
||||
"description": "CRAC unit goes offline — rack temperatures rise rapidly.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "crac-01",
|
||||
"targets": ["crac-01", "crac-02"],
|
||||
},
|
||||
{
|
||||
"name": "FAN_DEGRADATION",
|
||||
"label": "Fan Degradation",
|
||||
"description": "CRAC fan bearing wear — fan speed drops, ΔT rises over ~25 min.",
|
||||
"duration": "~25 min",
|
||||
"compound": False,
|
||||
"default_target": "crac-01",
|
||||
"targets": ["crac-01", "crac-02"],
|
||||
},
|
||||
{
|
||||
"name": "COMPRESSOR_FAULT",
|
||||
"label": "Compressor Fault",
|
||||
"description": "Compressor trips — unit drops to fan-only, cooling capacity collapses to ~8%.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "crac-01",
|
||||
"targets": ["crac-01", "crac-02"],
|
||||
},
|
||||
{
|
||||
"name": "DIRTY_FILTER",
|
||||
"label": "Dirty Filter",
|
||||
"description": "Filter fouling — ΔP rises, airflow and capacity degrade over time.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "crac-01",
|
||||
"targets": ["crac-01", "crac-02"],
|
||||
},
|
||||
{
|
||||
"name": "HIGH_TEMPERATURE",
|
||||
"label": "High Temperature",
|
||||
"description": "Gradual ambient heat rise — slower than a full cooling failure.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "hall-a",
|
||||
"targets": ["hall-a", "hall-b"],
|
||||
},
|
||||
{
|
||||
"name": "HUMIDITY_SPIKE",
|
||||
"label": "Humidity Spike",
|
||||
"description": "Humidity climbs — condensation / humidifier fault risk.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "hall-a",
|
||||
"targets": ["hall-a", "hall-b"],
|
||||
},
|
||||
{
|
||||
"name": "CHILLER_FAULT",
|
||||
"label": "Chiller Fault",
|
||||
"description": "Chiller plant trips — chilled water supply lost.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "chiller-01",
|
||||
"targets": ["chiller-01"],
|
||||
},
|
||||
# ── Power ────────────────────────────────────────────────────────────────
|
||||
{
|
||||
"name": "UPS_MAINS_FAILURE",
|
||||
"label": "UPS Mains Failure",
|
||||
"description": "Mains power lost — UPS switches to battery and drains.",
|
||||
"duration": "~60 min",
|
||||
"compound": False,
|
||||
"default_target": "ups-01",
|
||||
"targets": ["ups-01", "ups-02"],
|
||||
},
|
||||
{
|
||||
"name": "POWER_SPIKE",
|
||||
"label": "Power Spike",
|
||||
"description": "PDU load surges across a room by up to 50%.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "hall-a",
|
||||
"targets": ["hall-a", "hall-b"],
|
||||
},
|
||||
{
|
||||
"name": "RACK_OVERLOAD",
|
||||
"label": "Rack Overload",
|
||||
"description": "Single rack redlines at ~85–95% of rated 10 kW capacity.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "SG1A01.10",
|
||||
"targets": ["SG1A01.10", "SG1B01.10"],
|
||||
},
|
||||
{
|
||||
"name": "PHASE_IMBALANCE",
|
||||
"label": "Phase Imbalance",
|
||||
"description": "PDU phase A overloads, phase C drops — imbalance flag triggers.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "SG1A01.10/pdu",
|
||||
"targets": ["SG1A01.10/pdu", "SG1B01.10/pdu"],
|
||||
},
|
||||
{
|
||||
"name": "ATS_TRANSFER",
|
||||
"label": "ATS Transfer",
|
||||
"description": "Utility feed lost — ATS transfers load to generator.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "ats-01",
|
||||
"targets": ["ats-01"],
|
||||
},
|
||||
{
|
||||
"name": "GENERATOR_FAILURE",
|
||||
"label": "Generator Running",
|
||||
"description": "Generator starts and runs under load following a utility failure.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "gen-01",
|
||||
"targets": ["gen-01"],
|
||||
},
|
||||
{
|
||||
"name": "GENERATOR_LOW_FUEL",
|
||||
"label": "Generator Low Fuel",
|
||||
"description": "Generator fuel level drains to critical low.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "gen-01",
|
||||
"targets": ["gen-01"],
|
||||
},
|
||||
{
|
||||
"name": "GENERATOR_FAULT",
|
||||
"label": "Generator Fault",
|
||||
"description": "Generator fails — fault state, no output.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "gen-01",
|
||||
"targets": ["gen-01"],
|
||||
},
|
||||
# ── Environmental / Life Safety ──────────────────────────────────────────
|
||||
{
|
||||
"name": "LEAK_DETECTED",
|
||||
"label": "Leak Detected",
|
||||
"description": "Water leak sensor triggers a critical alarm.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "leak-01",
|
||||
"targets": ["leak-01", "leak-02", "leak-03"],
|
||||
},
|
||||
{
|
||||
"name": "VESDA_ALERT",
|
||||
"label": "VESDA Alert",
|
||||
"description": "Smoke obscuration rises into the Alert/Action band.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "vesda-hall-a",
|
||||
"targets": ["vesda-hall-a", "vesda-hall-b"],
|
||||
},
|
||||
{
|
||||
"name": "VESDA_FIRE",
|
||||
"label": "VESDA Fire",
|
||||
"description": "Smoke obscuration escalates to Fire level.",
|
||||
"duration": "ongoing",
|
||||
"compound": False,
|
||||
"default_target": "vesda-hall-a",
|
||||
"targets": ["vesda-hall-a", "vesda-hall-b"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ── Request / Response models ────────────────────────────────────────────────
|
||||
|
||||
class TriggerRequest(BaseModel):
|
||||
scenario: str
|
||||
target: Optional[str] = None
|
||||
|
||||
|
||||
# ── Endpoints ────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("")
|
||||
async def list_scenarios():
|
||||
return SCENARIOS
|
||||
|
||||
|
||||
@router.post("/trigger")
|
||||
async def trigger_scenario(body: TriggerRequest):
|
||||
payload = json.dumps({"scenario": body.scenario, "target": body.target})
|
||||
try:
|
||||
async with aiomqtt.Client(settings.MQTT_HOST, port=settings.MQTT_PORT) as client:
|
||||
await client.publish("bms/control/scenario", payload, qos=1)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=503, detail=f"MQTT unavailable: {e}")
|
||||
return {"ok": True, "scenario": body.scenario, "target": body.target}
|
||||
Loading…
Add table
Add a link
Reference in a new issue