261 lines
9.4 KiB
Python
261 lines
9.4 KiB
Python
"""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
|
||
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)
|
||
|
||
|
||
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"]),
|
||
grid_import_cashflow_czk=_num(r["grid_import_cashflow_czk"]),
|
||
grid_export_revenue_czk=_num(r["grid_export_revenue_czk"]),
|
||
grid_to_load_cost_czk=_num(r["grid_to_load_cost_czk"]),
|
||
grid_to_batt_cost_czk=_num(r["grid_to_batt_cost_czk"]),
|
||
)
|
||
|
||
|
||
@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,
|
||
ROUND(
|
||
SUM(
|
||
COALESCE(ai.actual_grid_import_wh, 0) / 1000.0
|
||
* COALESCE(ep.effective_buy_price_czk_kwh, 0)
|
||
),
|
||
2
|
||
) AS grid_import_cashflow_czk,
|
||
ROUND(
|
||
SUM(
|
||
COALESCE(ai.actual_grid_export_wh, 0) / 1000.0
|
||
* COALESCE(ep.effective_sell_price_czk_kwh, 0)
|
||
),
|
||
2
|
||
) AS grid_export_revenue_czk,
|
||
ROUND(
|
||
SUM(
|
||
COALESCE(ai.flow_grid_to_load_wh, 0) / 1000.0
|
||
* COALESCE(ep.effective_buy_price_czk_kwh, 0)
|
||
),
|
||
2
|
||
) AS grid_to_load_cost_czk,
|
||
ROUND(
|
||
SUM(
|
||
COALESCE(ai.flow_grid_to_batt_wh, 0) / 1000.0
|
||
* COALESCE(ep.effective_buy_price_czk_kwh, 0)
|
||
),
|
||
2
|
||
) AS grid_to_batt_cost_czk
|
||
FROM ems.audit_interval ai
|
||
LEFT JOIN ems.vw_site_effective_price ep
|
||
ON ep.site_id = ai.site_id
|
||
AND ep.interval_start = ai.interval_start
|
||
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
|
||
]
|