Files
ems/backend/app/routers/energy_flows.py
Dusan Vojacek f714cab0ab
All checks were successful
deploy / deploy (push) Successful in 8m52s
test / smoke-test (push) Successful in 5s
nova stranka flow a obsluha
2026-04-10 22:13:58 +02:00

222 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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
]