BMS/backend/api/routes/capacity.py
2026-03-19 11:32:17 +00:00

110 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from fastapi import APIRouter, Depends, Query
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from core.database import get_session
router = APIRouter()
ROOMS = {
"sg-01": [
{"room_id": "hall-a", "racks": [f"SG1A01.{i:02d}" for i in range(1, 21)] + [f"SG1A02.{i:02d}" for i in range(1, 21)], "crac_id": "crac-01"},
{"room_id": "hall-b", "racks": [f"SG1B01.{i:02d}" for i in range(1, 21)] + [f"SG1B02.{i:02d}" for i in range(1, 21)], "crac_id": "crac-02"},
]
}
# Rated capacity config — would be per-asset configurable in production
RACK_POWER_CAPACITY_KW = 10.0 # max kW per rack
ROOM_POWER_CAPACITY_KW = 400.0 # 40 racks × 10 kW
CRAC_COOLING_CAPACITY_KW = 160.0 # rated cooling per CRAC
RACK_U_TOTAL = 42
@router.get("/summary")
async def capacity_summary(
site_id: str = Query(...),
session: AsyncSession = Depends(get_session),
):
"""Per-rack and per-room capacity: power used vs rated, cooling load vs rated, rack space."""
result = await session.execute(text("""
SELECT DISTINCT ON (sensor_id)
rack_id, room_id, sensor_type, value
FROM readings
WHERE site_id = :site_id
AND sensor_type IN ('power_kw', 'temperature')
AND rack_id IS NOT NULL
AND recorded_at > NOW() - INTERVAL '10 minutes'
ORDER BY sensor_id, recorded_at DESC
"""), {"site_id": site_id})
rows = result.mappings().all()
# Index: rack_id → {power_kw, temperature, room_id}
rack_idx: dict[str, dict] = {}
for row in rows:
rid = row["rack_id"]
if rid not in rack_idx:
rack_idx[rid] = {"room_id": row["room_id"]}
if row["sensor_type"] == "power_kw":
rack_idx[rid]["power_kw"] = round(float(row["value"]), 2)
elif row["sensor_type"] == "temperature":
rack_idx[rid]["temperature"] = round(float(row["value"]), 1)
rooms_out = []
racks_out = []
for room in ROOMS.get(site_id, []):
room_id = room["room_id"]
room_power = 0.0
populated = 0
for rack_id in room["racks"]:
d = rack_idx.get(rack_id, {})
power = d.get("power_kw")
temp = d.get("temperature")
if power is not None:
room_power += power
populated += 1
power_pct = round((power / RACK_POWER_CAPACITY_KW) * 100, 1) if power is not None else None
racks_out.append({
"rack_id": rack_id,
"room_id": room_id,
"power_kw": power,
"power_capacity_kw": RACK_POWER_CAPACITY_KW,
"power_pct": power_pct,
"temp": temp,
})
room_power = round(room_power, 2)
rooms_out.append({
"room_id": room_id,
"power": {
"used_kw": room_power,
"capacity_kw": ROOM_POWER_CAPACITY_KW,
"pct": round((room_power / ROOM_POWER_CAPACITY_KW) * 100, 1),
"headroom_kw": round(ROOM_POWER_CAPACITY_KW - room_power, 2),
},
"cooling": {
"load_kw": room_power, # IT power ≈ heat generated
"capacity_kw": CRAC_COOLING_CAPACITY_KW,
"pct": round(min(100.0, (room_power / CRAC_COOLING_CAPACITY_KW) * 100), 1),
"headroom_kw": round(max(0.0, CRAC_COOLING_CAPACITY_KW - room_power), 2),
},
"space": {
"racks_total": len(room["racks"]),
"racks_populated": populated,
"pct": round((populated / len(room["racks"])) * 100, 1),
},
})
return {
"site_id": site_id,
"config": {
"rack_power_kw": RACK_POWER_CAPACITY_KW,
"room_power_kw": ROOM_POWER_CAPACITY_KW,
"crac_cooling_kw": CRAC_COOLING_CAPACITY_KW,
"rack_u_total": RACK_U_TOTAL,
},
"rooms": rooms_out,
"racks": racks_out,
}