Files
ems/backend/app/routers/plan.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

134 lines
4.3 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 aktivní plán a ruční přepočet."""
import json
import logging
from datetime import datetime, timezone
from typing import Annotated, Any, Literal
import asyncpg
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, ConfigDict, Field
from app.db_json import fetch_json
from app.deps import get_pg_pool
from services.control_exporter import export_setpoints
from services.planning_engine import run_plan_api
router = APIRouter(prefix="/sites/{site_id}/plan", tags=["plan"])
logger = logging.getLogger(__name__)
class RunPlanResponse(BaseModel):
run_id: int
solver_duration_ms: int
horizon_start: datetime
horizon_end: datetime
class PlanningIntervalDto(BaseModel):
"""Řádek `ems.planning_interval` v odpovědi aktivního plánu."""
model_config = ConfigDict(extra="allow")
interval_start: str
is_predicted_price: bool = Field(
default=False,
description=(
"True pokud solver pro slot použil predikovanou cenu (market_price_stats), "
"nikoli přesný řádek z vw_site_effective_price / OTE."
),
)
class CurrentPlanResponseModel(BaseModel):
run: dict[str, Any]
intervals: list[PlanningIntervalDto]
summary: dict[str, Any]
@router.get("/current", response_model=CurrentPlanResponseModel)
async def get_current_plan(
site_id: int,
pool: Annotated[asyncpg.Pool, Depends(get_pg_pool)],
) -> CurrentPlanResponseModel:
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")
bundle = await fetch_json(
conn,
"select ems.fn_plan_current_bundle($1::int)",
site_id,
)
if not isinstance(bundle, dict):
bundle = json.loads(bundle)
if bundle.get("error") == "no_active_plan":
raise HTTPException(status_code=404, detail="No active plan")
intervals_raw = bundle.get("intervals") or []
if not isinstance(intervals_raw, list):
intervals_raw = []
intervals = [PlanningIntervalDto.model_validate(d) for d in intervals_raw if isinstance(d, dict)]
return CurrentPlanResponseModel(
run=bundle.get("run") or {},
intervals=intervals,
summary=bundle.get("summary") or {},
)
@router.post("/run", response_model=RunPlanResponse)
async def post_run_plan(
site_id: int,
pool: Annotated[asyncpg.Pool, Depends(get_pg_pool)],
plan_type: Literal["daily", "rolling"] = Query("rolling", alias="type"),
) -> RunPlanResponse:
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")
days_with_prices = await conn.fetchval(
"select ems.fn_planning_future_price_days()",
)
if (days_with_prices or 0) < 1:
raise HTTPException(
status_code=422,
detail="Nejsou dostupné tržní ceny",
)
try:
run_id, solver_duration_ms = await run_plan_api(
site_id, plan_type, conn, triggered_by="api"
)
await export_setpoints(site_id, conn)
row = await fetch_json(
conn,
"select ems.fn_planning_run_horizon($1::int)",
run_id,
)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
except RuntimeError as e:
raise HTTPException(status_code=422, detail=str(e)) from e
except Exception as e:
logger.error("Plan run failed: %s", e, exc_info=True)
raise HTTPException(status_code=422, detail=str(e)) from e
if not isinstance(row, dict) or row.get("horizon_start") is None:
raise HTTPException(status_code=500, detail="Planning run row missing after insert")
return RunPlanResponse(
run_id=run_id,
solver_duration_ms=solver_duration_ms,
horizon_start=row["horizon_start"],
horizon_end=row["horizon_end"],
)