korkece fve predikce, grafy predikci
Some checks failed
CI and deploy / migration-check (push) Failing after 10s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-04-22 19:26:46 +02:00
parent ffe80679cc
commit 9ca4b4c577
10 changed files with 819 additions and 5 deletions

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import json
import logging
from datetime import date, datetime, timedelta
from datetime import date, datetime, timedelta, timezone
from typing import Annotated, Any
import asyncpg
@@ -522,3 +522,159 @@ async def get_site_forecast_pv_slots_range(
if not isinstance(slots, list):
slots = []
return {"slots": slots}
@router.get("/{site_id}/forecast/pv-slots-corrected")
async def get_site_forecast_pv_slots_range_corrected(
site_id: int,
db: Annotated[asyncpg.Pool, Depends(get_pg_pool)],
from_ts: datetime = Query(
...,
alias="from",
description="Začátek okna [from, to), typicky UTC zaokrouhlené na 15 min",
),
to_ts: datetime = Query(
...,
alias="to",
description="Konec polouzavřeného intervalu (max. cca 120 h za from)",
),
delta_from_ts: datetime | None = Query(
None,
alias="delta_from",
description="Začátek okna historie pro výpočet delta profilu (default: now-60d)",
),
delta_to_ts: datetime | None = Query(
None,
alias="delta_to",
description="Konec okna historie pro výpočet delta profilu (default: now)",
),
half_life_days: float = Query(
14,
ge=1,
le=90,
description="Half-life vážení (dny) pro delta profil",
),
threshold_w: int = Query(
150,
ge=0,
le=10_000,
description="Ignorovat sloty s nízkou výrobou (W) při odhadu profilu",
),
) -> dict[str, list[dict[str, Any]]]:
if to_ts <= from_ts:
raise HTTPException(status_code=422, detail="'to' must be after 'from'")
if to_ts - from_ts > timedelta(hours=120):
raise HTTPException(
status_code=422,
detail="Span between 'from' and 'to' must be at most 120 hours",
)
now = datetime.now(tz=timezone.utc)
delta_to = delta_to_ts or now
delta_from = delta_from_ts or (delta_to - timedelta(days=60))
async with db.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")
raw = await fetch_json(
conn,
"""
select ems.fn_forecast_pv_slots_range_corrected(
$1::int,
$2::timestamptz,
$3::timestamptz,
$4::timestamptz,
$5::timestamptz,
$6::numeric,
$7::int
)
""",
site_id,
from_ts,
to_ts,
delta_from,
delta_to,
half_life_days,
threshold_w,
)
slots = raw if isinstance(raw, list) else []
if not isinstance(slots, list):
slots = []
return {"slots": [s for s in slots if isinstance(s, dict)]}
@router.get("/{site_id}/timeseries/telemetry-15m")
async def get_site_telemetry_15m_range(
site_id: int,
db: Annotated[asyncpg.Pool, Depends(get_pg_pool)],
from_ts: datetime = Query(..., alias="from", description="Začátek okna [from, to)"),
to_ts: datetime = Query(..., alias="to", description="Konec okna [from, to)"),
) -> dict[str, list[dict[str, Any]]]:
if to_ts <= from_ts:
raise HTTPException(status_code=422, detail="'to' must be after 'from'")
if to_ts - from_ts > timedelta(days=60):
raise HTTPException(
status_code=422,
detail="Span between 'from' and 'to' must be at most 60 days",
)
async with db.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 conn.fetch(
"""
select
slot_start,
site_id,
avg_pv_w,
avg_load_w,
avg_grid_w,
avg_battery_w,
last_soc_pct,
sample_count
from ems.telemetry_inverter_15m
where site_id = $1
and slot_start >= $2::timestamptz
and slot_start < $3::timestamptz
order by slot_start asc
""",
site_id,
from_ts,
to_ts,
)
return {"slots": [record_to_dict(r) for r in rows]}
@router.get("/{site_id}/forecast/load-baseline-slots")
async def get_site_load_baseline_slots_range(
site_id: int,
db: Annotated[asyncpg.Pool, Depends(get_pg_pool)],
from_ts: datetime = Query(..., alias="from", description="Začátek okna [from, to)"),
to_ts: datetime = Query(..., alias="to", description="Konec okna [from, to)"),
) -> dict[str, list[dict[str, Any]]]:
if to_ts <= from_ts:
raise HTTPException(status_code=422, detail="'to' must be after 'from'")
if to_ts - from_ts > timedelta(days=60):
raise HTTPException(
status_code=422,
detail="Span between 'from' and 'to' must be at most 60 days",
)
async with db.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 conn.fetch(
"""
select interval_start, forecast_w, confidence_w
from ems.fn_get_baseline_forecast($1::int, $2::timestamptz, $3::timestamptz)
""",
site_id,
from_ts,
to_ts,
)
return {"slots": [record_to_dict(r) for r in rows]}