"""REST API – analýza energetických toků (modelované toky z audit_interval).""" from __future__ import annotations from datetime import date from typing import Annotated, Any import asyncpg from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from app.deps import get_pg_pool router = APIRouter( prefix="/sites/{site_id}/energy-flows", tags=["energy-flows"], ) class DailyEnergyFlows(BaseModel): day: date interval_count: int pv_production_kwh: float grid_import_kwh: float grid_export_kwh: float batt_charge_kwh: float batt_discharge_kwh: float load_kwh: float pv_to_load_kwh: float pv_to_batt_kwh: float pv_to_grid_kwh: float batt_to_load_kwh: float batt_to_grid_kwh: float grid_to_load_kwh: float grid_to_batt_kwh: float class DailyEnergyFlowsResponse(BaseModel): days: list[DailyEnergyFlows] class IntervalEnergyFlows(BaseModel): interval_start: str pv_production_kwh: float | None grid_import_kwh: float | None grid_export_kwh: float | None batt_charge_kwh: float | None batt_discharge_kwh: float | None load_kwh: float | None pv_to_load_kwh: float | None pv_to_batt_kwh: float | None pv_to_grid_kwh: float | None batt_to_load_kwh: float | None batt_to_grid_kwh: float | None grid_to_load_kwh: float | None grid_to_batt_kwh: float | None def _num(val: Any) -> float: if val is None: return 0.0 return float(val) def _wh_to_kwh(val: Any) -> float | None: if val is None: return None return round(float(val) / 1000.0, 4) async def _check_site(conn: asyncpg.Connection, site_id: int) -> None: ok = await conn.fetchval( "SELECT EXISTS(SELECT 1 FROM ems.site WHERE id = $1)", site_id ) if not ok: raise HTTPException(status_code=404, detail="Site not found") def _row_to_daily(r: Any) -> DailyEnergyFlows: return DailyEnergyFlows( day=r["day_local"], interval_count=int(r["interval_count"] or 0), pv_production_kwh=_num(r["pv_production_kwh"]), grid_import_kwh=_num(r["grid_import_kwh"]), grid_export_kwh=_num(r["grid_export_kwh"]), batt_charge_kwh=_num(r["batt_charge_kwh"]), batt_discharge_kwh=_num(r["batt_discharge_kwh"]), load_kwh=_num(r["load_kwh"]), pv_to_load_kwh=_num(r["pv_to_load_kwh"]), pv_to_batt_kwh=_num(r["pv_to_batt_kwh"]), pv_to_grid_kwh=_num(r["pv_to_grid_kwh"]), batt_to_load_kwh=_num(r["batt_to_load_kwh"]), batt_to_grid_kwh=_num(r["batt_to_grid_kwh"]), grid_to_load_kwh=_num(r["grid_to_load_kwh"]), grid_to_batt_kwh=_num(r["grid_to_batt_kwh"]), ) @router.get("/daily", response_model=DailyEnergyFlowsResponse) async def get_energy_flows_daily( site_id: int, db: Annotated[asyncpg.Pool, Depends(get_pg_pool)], month: str = Query( ..., description="YYYY-MM", pattern=r"^\d{4}-\d{2}$", ), ) -> DailyEnergyFlowsResponse: try: year, mon = month.split("-") month_start = date(int(year), int(mon), 1) if int(mon) == 12: month_end = date(int(year) + 1, 1, 1) else: month_end = date(int(year), int(mon) + 1, 1) except (ValueError, IndexError): raise HTTPException(status_code=400, detail="Invalid month, expected YYYY-MM") async with db.acquire() as conn: await _check_site(conn, site_id) rows = await conn.fetch( """ SELECT (date_trunc('day', ai.interval_start AT TIME ZONE 'Europe/Prague'))::date AS day_local, COUNT(*)::int AS interval_count, ROUND(SUM(COALESCE(ai.actual_pv_production_wh, 0)) / 1000, 3) AS pv_production_kwh, ROUND(SUM(COALESCE(ai.actual_grid_import_wh, 0)) / 1000, 3) AS grid_import_kwh, ROUND(SUM(COALESCE(ai.actual_grid_export_wh, 0)) / 1000, 3) AS grid_export_kwh, ROUND(SUM(COALESCE(ai.actual_batt_charge_wh, 0)) / 1000, 3) AS batt_charge_kwh, ROUND(SUM(COALESCE(ai.actual_batt_discharge_wh, 0)) / 1000, 3) AS batt_discharge_kwh, ROUND(SUM(COALESCE(ai.actual_load_consumption_wh, 0)) / 1000, 3) AS load_kwh, ROUND(SUM(COALESCE(ai.flow_pv_to_load_wh, 0)) / 1000, 3) AS pv_to_load_kwh, ROUND(SUM(COALESCE(ai.flow_pv_to_batt_wh, 0)) / 1000, 3) AS pv_to_batt_kwh, ROUND(SUM(COALESCE(ai.flow_pv_to_grid_wh, 0)) / 1000, 3) AS pv_to_grid_kwh, ROUND(SUM(COALESCE(ai.flow_batt_to_load_wh, 0)) / 1000, 3) AS batt_to_load_kwh, ROUND(SUM(COALESCE(ai.flow_batt_to_grid_wh, 0)) / 1000, 3) AS batt_to_grid_kwh, ROUND(SUM(COALESCE(ai.flow_grid_to_load_wh, 0)) / 1000, 3) AS grid_to_load_kwh, ROUND(SUM(COALESCE(ai.flow_grid_to_batt_wh, 0)) / 1000, 3) AS grid_to_batt_kwh FROM ems.audit_interval ai WHERE ai.site_id = $1 AND (date_trunc('day', ai.interval_start AT TIME ZONE 'Europe/Prague'))::date >= $2 AND (date_trunc('day', ai.interval_start AT TIME ZONE 'Europe/Prague'))::date < $3 GROUP BY 1 ORDER BY 1 """, site_id, month_start, month_end, ) return DailyEnergyFlowsResponse(days=[_row_to_daily(r) for r in rows]) @router.get("/daily/{day}/intervals", response_model=list[IntervalEnergyFlows]) async def get_energy_flows_intervals( site_id: int, day: date, db: Annotated[asyncpg.Pool, Depends(get_pg_pool)], ) -> list[IntervalEnergyFlows]: async with db.acquire() as conn: await _check_site(conn, site_id) rows = await conn.fetch( """ SELECT interval_start, actual_pv_production_wh, actual_grid_import_wh, actual_grid_export_wh, actual_batt_charge_wh, actual_batt_discharge_wh, actual_load_consumption_wh, flow_pv_to_load_wh, flow_pv_to_batt_wh, flow_pv_to_grid_wh, flow_batt_to_load_wh, flow_batt_to_grid_wh, flow_grid_to_load_wh, flow_grid_to_batt_wh FROM ems.audit_interval WHERE site_id = $1 AND (date_trunc('day', interval_start AT TIME ZONE 'Europe/Prague'))::date = $2 ORDER BY interval_start """, site_id, day, ) return [ IntervalEnergyFlows( interval_start=r["interval_start"].isoformat(), pv_production_kwh=_wh_to_kwh(r["actual_pv_production_wh"]), grid_import_kwh=_wh_to_kwh(r["actual_grid_import_wh"]), grid_export_kwh=_wh_to_kwh(r["actual_grid_export_wh"]), batt_charge_kwh=_wh_to_kwh(r["actual_batt_charge_wh"]), batt_discharge_kwh=_wh_to_kwh(r["actual_batt_discharge_wh"]), load_kwh=_wh_to_kwh(r["actual_load_consumption_wh"]), pv_to_load_kwh=_wh_to_kwh(r["flow_pv_to_load_wh"]), pv_to_batt_kwh=_wh_to_kwh(r["flow_pv_to_batt_wh"]), pv_to_grid_kwh=_wh_to_kwh(r["flow_pv_to_grid_wh"]), batt_to_load_kwh=_wh_to_kwh(r["flow_batt_to_load_wh"]), batt_to_grid_kwh=_wh_to_kwh(r["flow_batt_to_grid_wh"]), grid_to_load_kwh=_wh_to_kwh(r["flow_grid_to_load_wh"]), grid_to_batt_kwh=_wh_to_kwh(r["flow_grid_to_batt_wh"]), ) for r in rows ]