nova stranka flow a obsluha
This commit is contained in:
@@ -16,6 +16,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from app.db_json import record_to_dict
|
||||
from app.deps import set_pg_pool
|
||||
from app.routers.economics import router as economics_router
|
||||
from app.routers.energy_flows import router as energy_flows_router
|
||||
from app.routers.ev import router as ev_router
|
||||
from app.routers.full_status import router as full_status_router
|
||||
from app.routers.plan import router as plan_router
|
||||
@@ -526,6 +527,7 @@ app.include_router(plan_router, prefix="/api/v1")
|
||||
app.include_router(ev_router, prefix="/api/v1")
|
||||
app.include_router(full_status_router, prefix="/api/v1")
|
||||
app.include_router(economics_router, prefix="/api/v1")
|
||||
app.include_router(energy_flows_router, prefix="/api/v1")
|
||||
|
||||
sites_router = APIRouter(prefix="/api/v1/sites", tags=["sites"])
|
||||
|
||||
|
||||
221
backend/app/routers/energy_flows.py
Normal file
221
backend/app/routers/energy_flows.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""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
|
||||
]
|
||||
Reference in New Issue
Block a user