first commit
This commit is contained in:
commit
4b98219bf7
144 changed files with 31561 additions and 0 deletions
229
backend/api/routes/readings.py
Normal file
229
backend/api/routes/readings.py
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
from datetime import datetime, timezone, timedelta
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from core.database import get_session
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/latest")
|
||||
async def get_latest_readings(
|
||||
site_id: str = Query(...),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Most recent reading per sensor for a site (last 10 minutes)."""
|
||||
result = await session.execute(text("""
|
||||
SELECT DISTINCT ON (sensor_id)
|
||||
sensor_id, sensor_type, site_id, room_id, rack_id, value, unit, recorded_at
|
||||
FROM readings
|
||||
WHERE site_id = :site_id
|
||||
AND recorded_at > NOW() - INTERVAL '10 minutes'
|
||||
ORDER BY sensor_id, recorded_at DESC
|
||||
"""), {"site_id": site_id})
|
||||
return [dict(r) for r in result.mappings().all()]
|
||||
|
||||
|
||||
@router.get("/kpis")
|
||||
async def get_site_kpis(
|
||||
site_id: str = Query(...),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Aggregate KPIs for the overview dashboard."""
|
||||
power = await session.execute(text("""
|
||||
SELECT COALESCE(SUM(value), 0) AS total_power_kw
|
||||
FROM (
|
||||
SELECT DISTINCT ON (sensor_id) sensor_id, value
|
||||
FROM readings
|
||||
WHERE site_id = :site_id AND sensor_type = 'power_kw'
|
||||
AND recorded_at > NOW() - INTERVAL '5 minutes'
|
||||
ORDER BY sensor_id, recorded_at DESC
|
||||
) latest
|
||||
"""), {"site_id": site_id})
|
||||
|
||||
temp = await session.execute(text("""
|
||||
SELECT COALESCE(AVG(value), 0) AS avg_temp
|
||||
FROM (
|
||||
SELECT DISTINCT ON (sensor_id) sensor_id, value
|
||||
FROM readings
|
||||
WHERE site_id = :site_id AND sensor_type = 'temperature'
|
||||
AND recorded_at > NOW() - INTERVAL '5 minutes'
|
||||
ORDER BY sensor_id, recorded_at DESC
|
||||
) latest
|
||||
"""), {"site_id": site_id})
|
||||
|
||||
alarms = await session.execute(text("""
|
||||
SELECT COUNT(*) AS alarm_count
|
||||
FROM alarms
|
||||
WHERE site_id = :site_id AND state = 'active'
|
||||
"""), {"site_id": site_id})
|
||||
|
||||
total_kw = float(power.mappings().one()["total_power_kw"])
|
||||
avg_temp = float(temp.mappings().one()["avg_temp"])
|
||||
alarm_cnt = int(alarms.mappings().one()["alarm_count"])
|
||||
pue = round(total_kw / (total_kw * 0.87), 2) if total_kw > 0 else 0.0
|
||||
|
||||
return {
|
||||
"total_power_kw": round(total_kw, 1),
|
||||
"pue": pue,
|
||||
"avg_temperature": round(avg_temp, 1),
|
||||
"active_alarms": alarm_cnt,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/site-power-history")
|
||||
async def get_site_power_history(
|
||||
site_id: str = Query(...),
|
||||
hours: int = Query(1, ge=1, le=24),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Total power (kW) bucketed by 5 minutes — for the power trend chart."""
|
||||
from_time = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||||
try:
|
||||
result = await session.execute(text("""
|
||||
SELECT bucket, ROUND(SUM(avg_per_sensor)::numeric, 1) AS total_kw
|
||||
FROM (
|
||||
SELECT
|
||||
time_bucket('5 minutes', recorded_at) AS bucket,
|
||||
sensor_id,
|
||||
AVG(value) AS avg_per_sensor
|
||||
FROM readings
|
||||
WHERE site_id = :site_id
|
||||
AND sensor_type = 'power_kw'
|
||||
AND recorded_at > :from_time
|
||||
GROUP BY bucket, sensor_id
|
||||
) per_sensor
|
||||
GROUP BY bucket
|
||||
ORDER BY bucket ASC
|
||||
"""), {"site_id": site_id, "from_time": from_time})
|
||||
except Exception:
|
||||
result = await session.execute(text("""
|
||||
SELECT bucket, ROUND(SUM(avg_per_sensor)::numeric, 1) AS total_kw
|
||||
FROM (
|
||||
SELECT
|
||||
date_trunc('minute', recorded_at) AS bucket,
|
||||
sensor_id,
|
||||
AVG(value) AS avg_per_sensor
|
||||
FROM readings
|
||||
WHERE site_id = :site_id
|
||||
AND sensor_type = 'power_kw'
|
||||
AND recorded_at > :from_time
|
||||
GROUP BY bucket, sensor_id
|
||||
) per_sensor
|
||||
GROUP BY bucket
|
||||
ORDER BY bucket ASC
|
||||
"""), {"site_id": site_id, "from_time": from_time})
|
||||
return [dict(r) for r in result.mappings().all()]
|
||||
|
||||
|
||||
@router.get("/room-temp-history")
|
||||
async def get_room_temp_history(
|
||||
site_id: str = Query(...),
|
||||
hours: int = Query(1, ge=1, le=24),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Average temperature per room bucketed by 5 minutes — for the temp trend chart."""
|
||||
from_time = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||||
try:
|
||||
result = await session.execute(text("""
|
||||
SELECT bucket, room_id, ROUND(AVG(avg_per_rack)::numeric, 2) AS avg_temp
|
||||
FROM (
|
||||
SELECT
|
||||
time_bucket('5 minutes', recorded_at) AS bucket,
|
||||
sensor_id, room_id,
|
||||
AVG(value) AS avg_per_rack
|
||||
FROM readings
|
||||
WHERE site_id = :site_id
|
||||
AND sensor_type = 'temperature'
|
||||
AND room_id IS NOT NULL
|
||||
AND recorded_at > :from_time
|
||||
GROUP BY bucket, sensor_id, room_id
|
||||
) per_rack
|
||||
GROUP BY bucket, room_id
|
||||
ORDER BY bucket ASC
|
||||
"""), {"site_id": site_id, "from_time": from_time})
|
||||
except Exception:
|
||||
result = await session.execute(text("""
|
||||
SELECT bucket, room_id, ROUND(AVG(avg_per_rack)::numeric, 2) AS avg_temp
|
||||
FROM (
|
||||
SELECT
|
||||
date_trunc('minute', recorded_at) AS bucket,
|
||||
sensor_id, room_id,
|
||||
AVG(value) AS avg_per_rack
|
||||
FROM readings
|
||||
WHERE site_id = :site_id
|
||||
AND sensor_type = 'temperature'
|
||||
AND room_id IS NOT NULL
|
||||
AND recorded_at > :from_time
|
||||
GROUP BY bucket, sensor_id, room_id
|
||||
) per_rack
|
||||
GROUP BY bucket, room_id
|
||||
ORDER BY bucket ASC
|
||||
"""), {"site_id": site_id, "from_time": from_time})
|
||||
return [dict(r) for r in result.mappings().all()]
|
||||
|
||||
|
||||
@router.get("/room-status")
|
||||
async def get_room_status(
|
||||
site_id: str = Query(...),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Current per-room summary: avg temp, total power, rack count, alarm count."""
|
||||
temp = await session.execute(text("""
|
||||
SELECT room_id, ROUND(AVG(value)::numeric, 1) AS avg_temp
|
||||
FROM (
|
||||
SELECT DISTINCT ON (sensor_id) sensor_id, room_id, value
|
||||
FROM readings
|
||||
WHERE site_id = :site_id
|
||||
AND sensor_type = 'temperature'
|
||||
AND room_id IS NOT NULL
|
||||
AND recorded_at > NOW() - INTERVAL '10 minutes'
|
||||
ORDER BY sensor_id, recorded_at DESC
|
||||
) latest
|
||||
GROUP BY room_id
|
||||
"""), {"site_id": site_id})
|
||||
|
||||
power = await session.execute(text("""
|
||||
SELECT room_id, ROUND(SUM(value)::numeric, 1) AS total_kw
|
||||
FROM (
|
||||
SELECT DISTINCT ON (sensor_id) sensor_id, room_id, value
|
||||
FROM readings
|
||||
WHERE site_id = :site_id
|
||||
AND sensor_type = 'power_kw'
|
||||
AND room_id IS NOT NULL
|
||||
AND recorded_at > NOW() - INTERVAL '10 minutes'
|
||||
ORDER BY sensor_id, recorded_at DESC
|
||||
) latest
|
||||
GROUP BY room_id
|
||||
"""), {"site_id": site_id})
|
||||
|
||||
alarm_counts = await session.execute(text("""
|
||||
SELECT room_id, COUNT(*) AS alarm_count, MAX(severity) AS worst_severity
|
||||
FROM alarms
|
||||
WHERE site_id = :site_id AND state = 'active' AND room_id IS NOT NULL
|
||||
GROUP BY room_id
|
||||
"""), {"site_id": site_id})
|
||||
|
||||
temp_map = {r["room_id"]: float(r["avg_temp"]) for r in temp.mappings().all()}
|
||||
power_map = {r["room_id"]: float(r["total_kw"]) for r in power.mappings().all()}
|
||||
alarm_map = {r["room_id"]: (int(r["alarm_count"]), r["worst_severity"])
|
||||
for r in alarm_counts.mappings().all()}
|
||||
|
||||
rooms = sorted(set(list(temp_map.keys()) + list(power_map.keys())))
|
||||
result = []
|
||||
for room_id in rooms:
|
||||
avg_temp = temp_map.get(room_id, 0.0)
|
||||
alarm_cnt, ws = alarm_map.get(room_id, (0, None))
|
||||
status = "ok"
|
||||
if ws == "critical" or avg_temp >= 30:
|
||||
status = "critical"
|
||||
elif ws == "warning" or avg_temp >= 26:
|
||||
status = "warning"
|
||||
result.append({
|
||||
"room_id": room_id,
|
||||
"avg_temp": avg_temp,
|
||||
"total_kw": power_map.get(room_id, 0.0),
|
||||
"alarm_count": alarm_cnt,
|
||||
"status": status,
|
||||
})
|
||||
return result
|
||||
Loading…
Add table
Add a link
Reference in a new issue