Full-stack energy management system for Tianpu Daxing campus. - Frontend: React 19 + TypeScript + Ant Design + ECharts - Backend: FastAPI + SQLAlchemy + PostgreSQL/TimescaleDB - Features: PV monitoring, heat pump management, carbon tracking, alarms, reports Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
140 lines
5.0 KiB
Python
140 lines
5.0 KiB
Python
from datetime import datetime, timedelta, timezone
|
|
from fastapi import APIRouter, Depends
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, and_, text
|
|
from app.core.database import get_db
|
|
from app.core.deps import get_current_user
|
|
from app.models.device import Device
|
|
from app.models.energy import EnergyData, EnergyDailySummary
|
|
from app.models.alarm import AlarmEvent
|
|
from app.models.carbon import CarbonEmission
|
|
from app.models.user import User
|
|
|
|
router = APIRouter(prefix="/dashboard", tags=["大屏数据"])
|
|
|
|
|
|
@router.get("/overview")
|
|
async def get_overview(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
|
"""能源总览大屏核心数据"""
|
|
now = datetime.now(timezone.utc)
|
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
# 设备状态统计
|
|
device_stats_q = await db.execute(
|
|
select(Device.status, func.count(Device.id)).where(Device.is_active == True).group_by(Device.status)
|
|
)
|
|
device_stats = {row[0]: row[1] for row in device_stats_q.all()}
|
|
|
|
# 今日能耗汇总
|
|
daily_q = await db.execute(
|
|
select(
|
|
EnergyDailySummary.energy_type,
|
|
func.sum(EnergyDailySummary.total_consumption),
|
|
func.sum(EnergyDailySummary.total_generation),
|
|
).where(EnergyDailySummary.date >= today_start).group_by(EnergyDailySummary.energy_type)
|
|
)
|
|
energy_summary = {}
|
|
for row in daily_q.all():
|
|
energy_summary[row[0]] = {"consumption": row[1] or 0, "generation": row[2] or 0}
|
|
|
|
# 今日碳排放
|
|
carbon_q = await db.execute(
|
|
select(func.sum(CarbonEmission.emission), func.sum(CarbonEmission.reduction))
|
|
.where(CarbonEmission.date >= today_start)
|
|
)
|
|
carbon_row = carbon_q.first()
|
|
|
|
# 活跃告警数
|
|
alarm_count_q = await db.execute(
|
|
select(func.count(AlarmEvent.id)).where(AlarmEvent.status == "active")
|
|
)
|
|
active_alarms = alarm_count_q.scalar() or 0
|
|
|
|
# 最近告警
|
|
recent_alarms_q = await db.execute(
|
|
select(AlarmEvent).where(AlarmEvent.status == "active").order_by(AlarmEvent.triggered_at.desc()).limit(10)
|
|
)
|
|
recent_alarms = [
|
|
{"id": a.id, "title": a.title, "severity": a.severity, "device_id": a.device_id,
|
|
"triggered_at": str(a.triggered_at)}
|
|
for a in recent_alarms_q.scalars().all()
|
|
]
|
|
|
|
return {
|
|
"device_stats": {
|
|
"online": device_stats.get("online", 0),
|
|
"offline": device_stats.get("offline", 0),
|
|
"alarm": device_stats.get("alarm", 0),
|
|
"total": sum(device_stats.values()),
|
|
},
|
|
"energy_today": energy_summary,
|
|
"carbon": {
|
|
"emission": carbon_row[0] or 0 if carbon_row else 0,
|
|
"reduction": carbon_row[1] or 0 if carbon_row else 0,
|
|
},
|
|
"active_alarms": active_alarms,
|
|
"recent_alarms": recent_alarms,
|
|
}
|
|
|
|
|
|
@router.get("/realtime")
|
|
async def get_realtime_data(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
|
"""实时功率数据 - 获取最近的采集数据"""
|
|
now = datetime.now(timezone.utc)
|
|
five_min_ago = now - timedelta(minutes=5)
|
|
|
|
latest_q = await db.execute(
|
|
select(EnergyData).where(
|
|
and_(EnergyData.timestamp >= five_min_ago, EnergyData.data_type == "power")
|
|
).order_by(EnergyData.timestamp.desc()).limit(50)
|
|
)
|
|
data_points = latest_q.scalars().all()
|
|
|
|
pv_ids = await _get_pv_device_ids(db)
|
|
hp_ids = await _get_hp_device_ids(db)
|
|
pv_power = sum(d.value for d in data_points if d.device_id in pv_ids)
|
|
heatpump_power = sum(d.value for d in data_points if d.device_id in hp_ids)
|
|
|
|
return {
|
|
"timestamp": str(now),
|
|
"pv_power": round(pv_power, 2),
|
|
"heatpump_power": round(heatpump_power, 2),
|
|
"total_load": round(pv_power + heatpump_power, 2),
|
|
"grid_power": round(max(0, heatpump_power - pv_power), 2),
|
|
}
|
|
|
|
|
|
@router.get("/load-curve")
|
|
async def get_load_curve(
|
|
hours: int = 24,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
"""负荷曲线数据"""
|
|
now = datetime.now(timezone.utc)
|
|
start = now - timedelta(hours=hours)
|
|
|
|
result = await db.execute(
|
|
select(
|
|
func.date_trunc('hour', EnergyData.timestamp).label('hour'),
|
|
func.avg(EnergyData.value).label('avg_power'),
|
|
).where(
|
|
and_(EnergyData.timestamp >= start, EnergyData.data_type == "power")
|
|
).group_by(text('hour')).order_by(text('hour'))
|
|
)
|
|
return [{"time": str(row[0]), "power": round(row[1], 2)} for row in result.all()]
|
|
|
|
|
|
async def _get_pv_device_ids(db: AsyncSession):
|
|
result = await db.execute(
|
|
select(Device.id).where(Device.device_type == "pv_inverter", Device.is_active == True)
|
|
)
|
|
return [r[0] for r in result.fetchall()]
|
|
|
|
|
|
async def _get_hp_device_ids(db: AsyncSession):
|
|
result = await db.execute(
|
|
select(Device.id).where(Device.device_type == "heat_pump", Device.is_active == True)
|
|
)
|
|
return [r[0] for r in result.fetchall()]
|