110 lines
4.1 KiB
Python
110 lines
4.1 KiB
Python
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,
|
||
}
|