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>
145 lines
6.2 KiB
Python
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,
|
|
}
|