Files
ems/backend/app/routers/ev.py
Dusan Vojacek 93f883f5e0
Some checks failed
CI and deploy / migration-check (push) Successful in 5s
CI and deploy / deploy (push) Failing after 20s
sql first refactor
2026-04-19 20:02:20 +02:00

152 lines
4.7 KiB
Python
Raw Permalink 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 aktivní EV session a úprava deadline / target SoC."""
from __future__ import annotations
import json
from datetime import date, datetime
from typing import Annotated, Any
import asyncpg
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, field_validator
from app.db_json import fetch_json
from app.deps import get_pg_pool
router = APIRouter(prefix="/sites/{site_id}/ev", tags=["ev"])
class EvSessionPatchBody(BaseModel):
target_soc_pct: float | None = None
target_deadline: datetime | None = None
@field_validator("target_soc_pct")
@classmethod
def _soc_range(cls, v: float | None) -> float | None:
if v is not None and not (10 <= v <= 100):
raise ValueError("target_soc_pct must be between 10 and 100")
return v
class EvSessionPatchResponse(BaseModel):
success: bool = True
session_id: int
@router.get("/sessions/active")
async def get_active_ev_sessions(
site_id: int,
pool: Annotated[asyncpg.Pool, Depends(get_pg_pool)],
) -> list[dict[str, Any]]:
async with pool.acquire() as conn:
site_ok = await conn.fetchval(
"SELECT EXISTS(SELECT 1 FROM ems.site WHERE id = $1)", site_id
)
if not site_ok:
raise HTTPException(status_code=404, detail="Site not found")
rows = await fetch_json(
conn,
"select ems.fn_ev_sessions_active($1::int)",
site_id,
)
if not isinstance(rows, list):
rows = json.loads(rows) if isinstance(rows, str) else []
return [r for r in rows if isinstance(r, dict)]
@router.patch("/sessions/{session_id}", response_model=EvSessionPatchResponse)
async def patch_ev_session(
site_id: int,
session_id: int,
body: EvSessionPatchBody,
pool: Annotated[asyncpg.Pool, Depends(get_pg_pool)],
) -> EvSessionPatchResponse:
async with pool.acquire() as conn:
site_ok = await conn.fetchval(
"SELECT EXISTS(SELECT 1 FROM ems.site WHERE id = $1)", site_id
)
if not site_ok:
raise HTTPException(status_code=404, detail="Site not found")
patch = body.model_dump(exclude_unset=True)
raw = await fetch_json(
conn,
"select ems.fn_ev_session_apply_patch($1::int, $2::int, $3::jsonb)",
site_id,
session_id,
json.dumps(patch),
)
if not isinstance(raw, dict):
raw = json.loads(raw)
if not raw.get("success"):
raise HTTPException(status_code=404, detail="Session not found")
return EvSessionPatchResponse(success=True, session_id=int(raw["session_id"]))
class ArrivalHourItem(BaseModel):
hour: int
confidence_pct: int
samples: int
class ChargerTomorrowArrival(BaseModel):
tomorrow: list[ArrivalHourItem]
class EvArrivalPredictionResponse(BaseModel):
insufficient_data: bool
tomorrow_date: str
chargers: dict[str, ChargerTomorrowArrival]
@router.get("/arrival-prediction", response_model=EvArrivalPredictionResponse)
async def get_ev_arrival_prediction(
site_id: int,
pool: Annotated[asyncpg.Pool, Depends(get_pg_pool)],
) -> EvArrivalPredictionResponse:
async with pool.acquire() as conn:
raw = await fetch_json(
conn,
"select ems.fn_ev_arrival_prediction_bundle($1::int)",
site_id,
)
if not isinstance(raw, dict):
raw = json.loads(raw)
if raw.get("error") == "site_not_found":
raise HTTPException(status_code=404, detail="Site not found")
chargers: dict[str, ChargerTomorrowArrival] = {}
ch_raw = raw.get("chargers") or {}
if isinstance(ch_raw, dict):
for code, v in ch_raw.items():
if not isinstance(v, dict):
continue
tlist = v.get("tomorrow") or []
items: list[ArrivalHourItem] = []
if isinstance(tlist, list):
for it in tlist:
if not isinstance(it, dict):
continue
items.append(
ArrivalHourItem(
hour=int(it.get("hour") or 0),
confidence_pct=int(it.get("confidence_pct") or 0),
samples=int(it.get("samples") or 0),
)
)
chargers[str(code)] = ChargerTomorrowArrival(tomorrow=items)
td = raw.get("tomorrow_date")
if isinstance(td, date):
td_s = td.isoformat()
elif isinstance(td, datetime):
td_s = td.date().isoformat()
else:
td_s = str(td or "")
return EvArrivalPredictionResponse(
insufficient_data=bool(raw.get("insufficient_data")),
tomorrow_date=td_s,
chargers=chargers,
)