193 lines
6.3 KiB
Python
193 lines
6.3 KiB
Python
"""REST API – analýza energetických toků (modelované toky z audit_interval)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from datetime import date
|
||
from typing import Annotated, Any
|
||
|
||
import asyncpg
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from pydantic import BaseModel
|
||
|
||
from app.db_json import fetch_json
|
||
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
|
||
grid_import_cashflow_czk: float
|
||
grid_export_revenue_czk: float
|
||
grid_to_load_cost_czk: float
|
||
grid_to_batt_cost_czk: 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)
|
||
|
||
|
||
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 _parse_day(val: Any) -> date:
|
||
from datetime import datetime as _dt
|
||
|
||
if isinstance(val, _dt):
|
||
return val.date()
|
||
if isinstance(val, date):
|
||
return val
|
||
if isinstance(val, str):
|
||
return date.fromisoformat(val[:10])
|
||
raise ValueError(val)
|
||
|
||
|
||
@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)
|
||
raw = await fetch_json(
|
||
conn,
|
||
"select ems.fn_energy_flows_daily_month($1::int, $2::date, $3::date)",
|
||
site_id,
|
||
month_start,
|
||
month_end,
|
||
)
|
||
if not isinstance(raw, dict):
|
||
raw = json.loads(raw)
|
||
rows = raw.get("days") or []
|
||
days: list[DailyEnergyFlows] = []
|
||
for r in rows:
|
||
if not isinstance(r, dict):
|
||
continue
|
||
days.append(
|
||
DailyEnergyFlows(
|
||
day=_parse_day(r.get("day")),
|
||
interval_count=int(r.get("interval_count") or 0),
|
||
pv_production_kwh=_num(r.get("pv_production_kwh")),
|
||
grid_import_kwh=_num(r.get("grid_import_kwh")),
|
||
grid_export_kwh=_num(r.get("grid_export_kwh")),
|
||
batt_charge_kwh=_num(r.get("batt_charge_kwh")),
|
||
batt_discharge_kwh=_num(r.get("batt_discharge_kwh")),
|
||
load_kwh=_num(r.get("load_kwh")),
|
||
pv_to_load_kwh=_num(r.get("pv_to_load_kwh")),
|
||
pv_to_batt_kwh=_num(r.get("pv_to_batt_kwh")),
|
||
pv_to_grid_kwh=_num(r.get("pv_to_grid_kwh")),
|
||
batt_to_load_kwh=_num(r.get("batt_to_load_kwh")),
|
||
batt_to_grid_kwh=_num(r.get("batt_to_grid_kwh")),
|
||
grid_to_load_kwh=_num(r.get("grid_to_load_kwh")),
|
||
grid_to_batt_kwh=_num(r.get("grid_to_batt_kwh")),
|
||
grid_import_cashflow_czk=_num(r.get("grid_import_cashflow_czk")),
|
||
grid_export_revenue_czk=_num(r.get("grid_export_revenue_czk")),
|
||
grid_to_load_cost_czk=_num(r.get("grid_to_load_cost_czk")),
|
||
grid_to_batt_cost_czk=_num(r.get("grid_to_batt_cost_czk")),
|
||
)
|
||
)
|
||
return DailyEnergyFlowsResponse(days=days)
|
||
|
||
|
||
@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 fetch_json(
|
||
conn,
|
||
"select ems.fn_energy_flows_intervals_day($1::int, $2::date)",
|
||
site_id,
|
||
day,
|
||
)
|
||
if not isinstance(rows, list):
|
||
rows = json.loads(rows) if isinstance(rows, str) else []
|
||
out: list[IntervalEnergyFlows] = []
|
||
for r in rows:
|
||
if not isinstance(r, dict):
|
||
continue
|
||
ist = r.get("interval_start")
|
||
out.append(
|
||
IntervalEnergyFlows(
|
||
interval_start=ist if isinstance(ist, str) else str(ist),
|
||
pv_production_kwh=r.get("pv_production_kwh"),
|
||
grid_import_kwh=r.get("grid_import_kwh"),
|
||
grid_export_kwh=r.get("grid_export_kwh"),
|
||
batt_charge_kwh=r.get("batt_charge_kwh"),
|
||
batt_discharge_kwh=r.get("batt_discharge_kwh"),
|
||
load_kwh=r.get("load_kwh"),
|
||
pv_to_load_kwh=r.get("pv_to_load_kwh"),
|
||
pv_to_batt_kwh=r.get("pv_to_batt_kwh"),
|
||
pv_to_grid_kwh=r.get("pv_to_grid_kwh"),
|
||
batt_to_load_kwh=r.get("batt_to_load_kwh"),
|
||
batt_to_grid_kwh=r.get("batt_to_grid_kwh"),
|
||
grid_to_load_kwh=r.get("grid_to_load_kwh"),
|
||
grid_to_batt_kwh=r.get("grid_to_batt_kwh"),
|
||
)
|
||
)
|
||
return out
|