Files
tianpu-ems/backend/app/api/v1/energy.py
Du Wenbo f53a610a19 Initial commit: Tianpu Zero-Carbon EMS Platform
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>
2026-04-01 13:36:06 +08:00

145 lines
6.2 KiB
Python

from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_, text
from pydantic import BaseModel
from app.core.database import get_db
from app.core.deps import get_current_user
from app.models.energy import EnergyData, EnergyDailySummary
from app.models.user import User
router = APIRouter(prefix="/energy", tags=["能耗数据"])
@router.get("/history")
async def query_history(
device_id: int | None = None,
data_type: str = "power",
start_time: str | None = None,
end_time: str | None = None,
granularity: str = Query("hour", pattern="^(raw|5min|hour|day)$"),
page: int = Query(1, ge=1),
page_size: int = Query(100, ge=1, le=1000),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""历史数据查询"""
query = select(EnergyData).where(EnergyData.data_type == data_type)
if device_id:
query = query.where(EnergyData.device_id == device_id)
if start_time:
query = query.where(EnergyData.timestamp >= start_time)
if end_time:
query = query.where(EnergyData.timestamp <= end_time)
if granularity == "raw":
query = query.order_by(EnergyData.timestamp.desc()).offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
return [{"timestamp": str(d.timestamp), "value": d.value, "unit": d.unit, "device_id": d.device_id}
for d in result.scalars().all()]
else:
if granularity == "5min":
time_bucket = func.to_timestamp(
func.floor(func.extract('epoch', EnergyData.timestamp) / 300) * 300
).label('time_bucket')
elif granularity == "hour":
time_bucket = func.date_trunc('hour', EnergyData.timestamp).label('time_bucket')
else: # day
time_bucket = func.date_trunc('day', EnergyData.timestamp).label('time_bucket')
agg_query = select(
time_bucket,
func.avg(EnergyData.value).label('avg_value'),
func.max(EnergyData.value).label('max_value'),
func.min(EnergyData.value).label('min_value'),
).where(EnergyData.data_type == data_type)
if device_id:
agg_query = agg_query.where(EnergyData.device_id == device_id)
if start_time:
agg_query = agg_query.where(EnergyData.timestamp >= start_time)
if end_time:
agg_query = agg_query.where(EnergyData.timestamp <= end_time)
agg_query = agg_query.group_by(text('time_bucket')).order_by(text('time_bucket'))
result = await db.execute(agg_query)
return [{"time": str(r[0]), "avg": round(r[1], 2), "max": round(r[2], 2), "min": round(r[3], 2)}
for r in result.all()]
@router.get("/daily-summary")
async def daily_summary(
start_date: str | None = None,
end_date: str | None = None,
energy_type: str | None = None,
device_id: int | None = None,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""每日能耗汇总"""
query = select(EnergyDailySummary)
if start_date:
query = query.where(EnergyDailySummary.date >= start_date)
if end_date:
query = query.where(EnergyDailySummary.date <= end_date)
if energy_type:
query = query.where(EnergyDailySummary.energy_type == energy_type)
if device_id:
query = query.where(EnergyDailySummary.device_id == device_id)
query = query.order_by(EnergyDailySummary.date.desc()).limit(365)
result = await db.execute(query)
return [{
"date": str(s.date), "device_id": s.device_id, "energy_type": s.energy_type,
"consumption": s.total_consumption, "generation": s.total_generation,
"peak_power": s.peak_power, "avg_power": s.avg_power,
"operating_hours": s.operating_hours, "cost": s.cost, "carbon_emission": s.carbon_emission,
} for s in result.scalars().all()]
@router.get("/comparison")
async def energy_comparison(
device_id: int | None = None,
energy_type: str = "electricity",
period: str = Query("month", pattern="^(day|week|month|year)$"),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""能耗同比环比分析"""
now = datetime.now(timezone.utc)
if period == "day":
current_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
prev_start = current_start - timedelta(days=1)
yoy_start = current_start.replace(year=current_start.year - 1)
elif period == "week":
current_start = now - timedelta(days=now.weekday())
current_start = current_start.replace(hour=0, minute=0, second=0, microsecond=0)
prev_start = current_start - timedelta(weeks=1)
yoy_start = current_start.replace(year=current_start.year - 1)
elif period == "month":
current_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
prev_start = (current_start - timedelta(days=1)).replace(day=1)
yoy_start = current_start.replace(year=current_start.year - 1)
else:
current_start = now.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
prev_start = current_start.replace(year=current_start.year - 1)
yoy_start = prev_start
async def sum_consumption(start, end):
q = select(func.sum(EnergyDailySummary.total_consumption)).where(
and_(EnergyDailySummary.date >= start, EnergyDailySummary.date < end,
EnergyDailySummary.energy_type == energy_type)
)
if device_id:
q = q.where(EnergyDailySummary.device_id == device_id)
r = await db.execute(q)
return r.scalar() or 0
current = await sum_consumption(current_start, now)
previous = await sum_consumption(prev_start, current_start)
yoy = await sum_consumption(yoy_start, yoy_start.replace(year=yoy_start.year + 1))
return {
"current": round(current, 2),
"previous": round(previous, 2),
"yoy": round(yoy, 2),
"mom_change": round((current - previous) / previous * 100, 1) if previous else 0,
"yoy_change": round((current - yoy) / yoy * 100, 1) if yoy else 0,
}