88 lines
5 KiB
Python
88 lines
5 KiB
Python
"""
|
|
DemoBMS Scenario Runner
|
|
Triggers demo scenarios by publishing a control message to the MQTT broker.
|
|
|
|
Usage:
|
|
python scenarios/runner.py --scenario COOLING_FAILURE --target crac-01
|
|
python scenarios/runner.py --scenario FAN_DEGRADATION --target crac-02
|
|
python scenarios/runner.py --scenario HIGH_TEMPERATURE --target hall-a
|
|
python scenarios/runner.py --scenario HUMIDITY_SPIKE --target hall-b
|
|
python scenarios/runner.py --scenario UPS_MAINS_FAILURE --target ups-01
|
|
python scenarios/runner.py --scenario POWER_SPIKE --target hall-a
|
|
python scenarios/runner.py --scenario RACK_OVERLOAD --target rack-A05/pdu
|
|
python scenarios/runner.py --scenario LEAK_DETECTED --target leak-01
|
|
python scenarios/runner.py --scenario RESET
|
|
python scenarios/runner.py --scenario RESET --target ups-01
|
|
"""
|
|
import sys
|
|
import os
|
|
import asyncio
|
|
import json
|
|
import argparse
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
import aiomqtt
|
|
from config import MQTT_HOST, MQTT_PORT
|
|
|
|
SCENARIOS = {
|
|
# Cooling
|
|
"COOLING_FAILURE": "CRAC unit goes offline — rack temperatures rise rapidly (target: crac-01 / crac-02)",
|
|
"FAN_DEGRADATION": "CRAC fan bearing wear — fan speed drops, ΔT rises over ~25 min (target: crac-01 / crac-02)",
|
|
"COMPRESSOR_FAULT": "Compressor trips — unit drops to fan-only, cooling capacity collapses to ~8% (target: crac-01 / crac-02)",
|
|
"DIRTY_FILTER": "Filter fouling — ΔP rises, airflow and capacity degrade over time (target: crac-01 / crac-02)",
|
|
"HIGH_TEMPERATURE": "Gradual ambient heat rise, e.g. summer overload — slower than COOLING_FAILURE (target: hall-a / hall-b)",
|
|
"HUMIDITY_SPIKE": "Humidity climbs — condensation / humidifier fault risk (target: hall-a / hall-b)",
|
|
"CHILLER_FAULT": "Chiller plant trips — chilled water supply lost (target: chiller-01)",
|
|
# Power
|
|
"UPS_MAINS_FAILURE": "Mains power lost — UPS switches to battery and drains (target: ups-01 / ups-02)",
|
|
"POWER_SPIKE": "PDU load surges across a room by up to 50% (target: hall-a / hall-b / rack-XXX/pdu)",
|
|
"RACK_OVERLOAD": "Single rack redlines at ~85-95% of rated 10 kW capacity (target: rack-XXX/pdu)",
|
|
"PHASE_IMBALANCE": "PDU phase A overloads, phase C drops — imbalance flag triggers (target: rack-XXX/pdu)",
|
|
"ATS_TRANSFER": "Utility feed lost — ATS transfers load to generator (target: ats-01)",
|
|
"GENERATOR_FAILURE": "Generator starts and runs under load following utility failure (target: gen-01)",
|
|
"GENERATOR_LOW_FUEL": "Generator fuel level drains to critical low (target: gen-01)",
|
|
"GENERATOR_FAULT": "Generator fails to start — fault state, no output (target: gen-01)",
|
|
# Environmental / Life Safety
|
|
"LEAK_DETECTED": "Water leak sensor triggers a critical alarm (target: leak-01 / leak-02 / leak-03)",
|
|
"VESDA_ALERT": "Smoke obscuration rises into Alert/Action band (target: vesda-hall-a / vesda-hall-b)",
|
|
"VESDA_FIRE": "Smoke obscuration escalates to Fire level (target: vesda-hall-a / vesda-hall-b)",
|
|
# Control
|
|
"RESET": "Return all bots (or a specific target) to normal operation",
|
|
# ── Compound (multi-bot, time-sequenced) ─────────────────────────────────
|
|
"HOT_NIGHT": "CRAC-01 trips → temps rise → power spikes → CRAC-02 degrades → VESDA alert [~10 min]",
|
|
"GENERATOR_TEST_GONE_WRONG": "ATS transfers → generator runs low on fuel → faults → UPS takes over [~16 min]",
|
|
"SLOW_BURN": "Dirty filter → creeping temp+humidity → VESDA alert → CRAC protection trips [~30 min]",
|
|
"LAST_RESORT": "Utility fails → generator starts → faults at 2 min → UPS overloads → VESDA fire [~9 min]",
|
|
}
|
|
|
|
|
|
async def trigger(scenario: str, target: str | None) -> None:
|
|
async with aiomqtt.Client(MQTT_HOST, port=MQTT_PORT) as client:
|
|
payload = json.dumps({"scenario": scenario, "target": target})
|
|
await client.publish("bms/control/scenario", payload, qos=1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="DemoBMS Scenario Runner")
|
|
parser.add_argument("--scenario", choices=list(SCENARIOS.keys()), required=False)
|
|
parser.add_argument(
|
|
"--target",
|
|
help="Device ID to target (e.g. rack-A05/pdu, crac-01, ups-01, hall-a, leak-01). Omit for all.",
|
|
default=None,
|
|
)
|
|
parser.add_argument("--list", action="store_true", help="List all available scenarios and exit")
|
|
args = parser.parse_args()
|
|
|
|
if args.list or not args.scenario:
|
|
print("\nAvailable scenarios:\n")
|
|
for name, desc in SCENARIOS.items():
|
|
print(f" {name:<22} {desc}")
|
|
print()
|
|
sys.exit(0)
|
|
|
|
print(f"\n Scenario : {args.scenario}")
|
|
print(f" Target : {args.target or 'ALL bots'}")
|
|
print(f" Effect : {SCENARIOS[args.scenario]}\n")
|
|
|
|
asyncio.run(trigger(args.scenario, args.target))
|
|
print("Done. Watch the dashboard for changes.")
|