dalsi uprava vypoctu delty (ignorujeme orezane vyroby)
Some checks failed
CI and deploy / migration-check (push) Failing after 17s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-04-22 22:42:12 +02:00
parent 568b584748
commit 1dfab8c7a1
11 changed files with 174 additions and 12 deletions

View File

@@ -98,6 +98,8 @@ Projekt je **SQL-first**: doménová logika, agregace, joiny mezi tabulkami a st
13. **Endpoint `GET …/forecast/pv`** vrací `DISTINCT ON (interval_start, pv_array_id)` seřazené podle nejnovějšího `forecast_pv_run.created_at`, aby UI nemělo duplikáty slotů; plná historie běhů zůstává v tabulkách. 13. **Endpoint `GET …/forecast/pv`** vrací `DISTINCT ON (interval_start, pv_array_id)` seřazené podle nejnovějšího `forecast_pv_run.created_at`, aby UI nemělo duplikáty slotů; plná historie běhů zůstává v tabulkách.
13a. **PV delta kalibrace:** `GET …/forecast/pv-delta-profile` vrací JSON z `fn_pv_forecast_delta_profile`; `GET …/configuration` obsahuje `pv_forecast_calibration` z `ems.site_pv_forecast_calibration`; `PATCH …/configuration/pv-forecast-calibration` mění cutoff / policy / přepsání parametrů delty. Telemetrie `telemetry_inverter.is_export_limited` / `pv_derating_flags` (V058) řídí vyloučení slotu z učení v `fn_fill_forecast_accuracy` (`telemetry_derating`).
14. **Příchod a odjezd EV** detekuje `telemetry_collector` z telemetrie nabíječky: přechod `available``preparing` / `charging` (resp. jakýkoli stav ≠ `available`) znamená příjezd; přechod na `available` uzavře `ev_session`. Tabulka `ev_arrival_stats` se při příjezdu doplňuje přes `fn_update_ev_arrival_stats` a **nemá se mazat** (dlouhodobá historická statistika). 14. **Příchod a odjezd EV** detekuje `telemetry_collector` z telemetrie nabíječky: přechod `available``preparing` / `charging` (resp. jakýkoli stav ≠ `available`) znamená příjezd; přechod na `available` uzavře `ev_session`. Tabulka `ev_arrival_stats` se při příjezdu doplňuje přes `fn_update_ev_arrival_stats` a **nemá se mazat** (dlouhodobá historická statistika).
15. **Bazální spotřeba** = `load_power_w` minus řízené zátěže (součet EV z `telemetry_ev_charger`, TČ z `telemetry_heat_pump`). Tabulka `consumption_baseline_stats` se plní denně (APScheduler 00:30) přes `fn_update_baseline_stats`. **Solver** načítá průměr bazálu z `consumption_baseline_stats` (DOW + hodina v Europe/Prague), ne z `consumption_baseline_interval`. 15. **Bazální spotřeba** = `load_power_w` minus řízené zátěže (součet EV z `telemetry_ev_charger`, TČ z `telemetry_heat_pump`). Tabulka `consumption_baseline_stats` se plní denně (APScheduler 00:30) přes `fn_update_baseline_stats`. **Solver** načítá průměr bazálu z `consumption_baseline_stats` (DOW + hodina v Europe/Prague), ne z `consumption_baseline_interval`.
@@ -132,12 +134,13 @@ Projekt je **SQL-first**: doménová logika, agregace, joiny mezi tabulkami a st
| `asset_heat_pump` | TČ (výkon, COP ref, limity běhu, TUV parametry). | | `asset_heat_pump` | TČ (výkon, COP ref, limity běhu, TUV parametry). |
| `asset_vehicle` | Vozidlo (kapacita, max AC výkon, default target SoC/deadline). | | `asset_vehicle` | Vozidlo (kapacita, max AC výkon, default target SoC/deadline). |
| `market_interval_price` | Raw spot OTE (15min), bez marží. | | `market_interval_price` | Raw spot OTE (15min), bez marží. |
| `telemetry_inverter` | 1min telemetrie střídače (Timescale). | | `telemetry_inverter` | 1min telemetrie střídače (Timescale); volitelně `is_export_limited`, `pv_derating_flags` pro vyloučení slotu z učení delty. |
| `telemetry_ev_charger` | 1min telemetrie nabíječky (Timescale). | | `telemetry_ev_charger` | 1min telemetrie nabíječky (Timescale). |
| `telemetry_heat_pump` | 1min telemetrie TČ (Timescale). | | `telemetry_heat_pump` | 1min telemetrie TČ (Timescale). |
| `forecast_pv_run` | Metadata běhu predikce FVE. | | `forecast_pv_run` | Metadata běhu predikce FVE. |
| `forecast_pv_interval` | Predikovaný výkon FVE po 15min (Timescale). | | `forecast_pv_interval` | Predikovaný výkon FVE po 15min (Timescale). |
| `forecast_accuracy` | Řádky přesnosti predikce vs telemetrie po 15min (per run); doplňuje `fn_fill_forecast_accuracy`. | | `forecast_accuracy` | Řádky přesnosti predikce vs telemetrie po 15min (per run); doplňuje `fn_fill_forecast_accuracy`. |
| `site_pv_forecast_calibration` | Per site: cutoff učení delty, policy škrcení, přepsání parametrů `fn_pv_forecast_delta_profile`. |
| `forecast_weather_interval` | Počasí 15min pro site (Timescale). | | `forecast_weather_interval` | Počasí 15min pro site (Timescale). |
| `forecast_correction_log` | Log korekcí forecastu vs skutečnost při rolling replanu. | | `forecast_correction_log` | Log korekcí forecastu vs skutečnost při rolling replanu. |
| `planning_run` | Jeden běh plánovače (daily/rolling/manual, stav, parametry solveru). | | `planning_run` | Jeden běh plánovače (daily/rolling/manual, stav, parametry solveru). |

View File

@@ -8,7 +8,7 @@ from typing import Annotated, Any
import asyncpg import asyncpg
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field from pydantic import BaseModel, ConfigDict, Field
from app.db_json import fetch_json from app.db_json import fetch_json
from app.deps import get_pg_pool from app.deps import get_pg_pool
@@ -16,6 +16,20 @@ from app.deps import get_pg_pool
router = APIRouter(prefix="/sites/{site_id}", tags=["sites"]) router = APIRouter(prefix="/sites/{site_id}", tags=["sites"])
class PvForecastCalibrationPatch(BaseModel):
"""Částečná úprava `ems.site_pv_forecast_calibration`. Vynechané klíče = beze změny."""
model_config = ConfigDict(extra="forbid")
delta_learn_min_ts: datetime | None = None
pv_curtailment_policy_effective_from: datetime | None = None
top_n_days: int | None = Field(default=None, ge=0, le=31)
non_top_day_factor: float | None = Field(default=None, ge=0, le=1)
day_weight_gamma: float | None = Field(default=None, ge=0.25, le=8)
half_life_days: float | None = Field(default=None, ge=1, le=90)
threshold_w: int | None = Field(default=None, ge=0, le=10_000)
class InverterModbusCurrentCapsBody(BaseModel): class InverterModbusCurrentCapsBody(BaseModel):
"""Tvrdý strop proudu pro zápis Deye reg 108/109 (A); NULL ve JSONu = smaž strop v DB.""" """Tvrdý strop proudu pro zápis Deye reg 108/109 (A); NULL ve JSONu = smaž strop v DB."""
@@ -77,6 +91,76 @@ async def get_site_configuration(
return raw return raw
@router.patch("/configuration/pv-forecast-calibration")
async def patch_pv_forecast_calibration(
site_id: int,
body: PvForecastCalibrationPatch,
pool: Annotated[asyncpg.Pool, Depends(get_pg_pool)],
) -> dict[str, Any]:
"""Aktualizace kalibrace PV delty (`ems.site_pv_forecast_calibration`)."""
updates = body.model_dump(exclude_unset=True)
if not updates:
raise HTTPException(status_code=400, detail="No fields to update")
if updates.get("delta_learn_min_ts") is None and "delta_learn_min_ts" in updates:
raise HTTPException(
status_code=422,
detail="delta_learn_min_ts cannot be null (column is NOT NULL)",
)
allowed = {
"delta_learn_min_ts",
"pv_curtailment_policy_effective_from",
"top_n_days",
"non_top_day_factor",
"day_weight_gamma",
"half_life_days",
"threshold_w",
}
bad = set(updates) - allowed
if bad:
raise HTTPException(status_code=400, detail=f"Unknown fields: {sorted(bad)}")
cols = list(updates.keys())
set_parts: list[str] = []
args: list[Any] = [site_id]
for i, col in enumerate(cols, start=2):
set_parts.append(f"{col} = ${i}")
args.append(updates[col])
set_sql = ", ".join(set_parts) + ", updated_at = now()"
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")
n = await conn.execute(
f"""
UPDATE ems.site_pv_forecast_calibration
SET {set_sql}
WHERE site_id = $1
""",
*args,
)
if n == "UPDATE 0":
raise HTTPException(
status_code=404,
detail="PV forecast calibration row missing; run migration V057",
)
row = await conn.fetchrow(
"""
SELECT to_jsonb(c.*) AS j
FROM ems.site_pv_forecast_calibration c
WHERE c.site_id = $1
""",
site_id,
)
raw = row["j"] if row else {}
if not isinstance(raw, dict):
raw = json.loads(raw)
return raw
@router.patch("/inverters/{inverter_id}/modbus-current-caps") @router.patch("/inverters/{inverter_id}/modbus-current-caps")
async def patch_inverter_modbus_current_caps( async def patch_inverter_modbus_current_caps(
site_id: int, site_id: int,

View File

@@ -0,0 +1,12 @@
-- Volitelné flagy pro vyloučení „škrcených“ slotů z učení PV delty (fáze 2 plánu kalibrace).
-- Plní collector podle režimu / registrů (145/179 apod.); dokud NULL, R__022 je ignoruje.
ALTER TABLE ems.telemetry_inverter
ADD COLUMN IF NOT EXISTS is_export_limited boolean NULL,
ADD COLUMN IF NOT EXISTS pv_derating_flags int NULL;
COMMENT ON COLUMN ems.telemetry_inverter.is_export_limited IS
'TRUE = interval indikuje omezení exportu / odpojení GEN (např. cut-off mikroinvertorů); fn_fill_forecast_accuracy může vyloučit slot z učení.';
COMMENT ON COLUMN ems.telemetry_inverter.pv_derating_flags IS
'Bitová maska nebo enum z režimu střídače (derating); <> 0 může vést k vyloučení slotu z učení delty.';

View File

@@ -27,16 +27,16 @@ BEGIN
/ 3600.0, 2 / 3600.0, 2
) AS lead_time_hours, ) AS lead_time_hours,
CASE CASE
WHEN v.is_curtailed_learning_slot THEN NULL WHEN v.exclude_actual_for_learning THEN NULL
ELSE slot.avg_actual_w::INT ELSE slot.avg_actual_w::INT
END AS actual_power_w, END AS actual_power_w,
now() AS actual_filled_at, now() AS actual_filled_at,
CASE CASE
WHEN v.is_curtailed_learning_slot THEN NULL WHEN v.exclude_actual_for_learning THEN NULL
ELSE fpi.power_w - COALESCE(slot.avg_actual_w::INT, 0) ELSE fpi.power_w - COALESCE(slot.avg_actual_w::INT, 0)
END AS error_w, END AS error_w,
CASE CASE
WHEN v.is_curtailed_learning_slot THEN NULL WHEN v.exclude_actual_for_learning THEN NULL
WHEN slot.avg_actual_w IS NOT NULL WHEN slot.avg_actual_w IS NOT NULL
AND slot.avg_actual_w > 0 AND slot.avg_actual_w > 0
THEN ROUND( THEN ROUND(
@@ -85,21 +85,34 @@ BEGIN
AND l.new_state IS FALSE AND l.new_state IS FALSE
) )
) )
) AS is_curtailed_learning_slot ) AS is_curtailed_learning_slot,
EXISTS (
SELECT 1
FROM ems.telemetry_inverter ti_d
WHERE ti_d.site_id = fpr.site_id
AND ti_d.measured_at >= fpi.interval_start
AND ti_d.measured_at < fpi.interval_start + INTERVAL '15 minutes'
AND (
coalesce(ti_d.is_export_limited, false) IS TRUE
OR (ti_d.pv_derating_flags IS NOT NULL AND ti_d.pv_derating_flags <> 0)
)
) AS is_telemetry_derated_slot
) flags ON true ) flags ON true
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
SELECT SELECT
CASE CASE
WHEN flags.before_learn_cutoff THEN false WHEN flags.before_learn_cutoff THEN false
WHEN flags.is_curtailed_learning_slot THEN false WHEN flags.is_curtailed_learning_slot THEN false
WHEN flags.is_telemetry_derated_slot THEN false
ELSE true ELSE true
END AS learning_eligible, END AS learning_eligible,
CASE CASE
WHEN flags.before_learn_cutoff THEN 'before_delta_learn_min' WHEN flags.before_learn_cutoff THEN 'before_delta_learn_min'
WHEN flags.is_telemetry_derated_slot THEN 'telemetry_derating'
WHEN flags.is_curtailed_learning_slot THEN 'curtailment_or_export_cutoff' WHEN flags.is_curtailed_learning_slot THEN 'curtailment_or_export_cutoff'
ELSE NULL ELSE NULL
END AS learning_exclude_reason, END AS learning_exclude_reason,
flags.is_curtailed_learning_slot (flags.is_curtailed_learning_slot OR flags.is_telemetry_derated_slot) AS exclude_actual_for_learning
) v ON true ) v ON true
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
SELECT AVG( SELECT AVG(
@@ -133,7 +146,8 @@ $$;
COMMENT ON FUNCTION ems.fn_fill_forecast_accuracy(INT, INT) IS COMMENT ON FUNCTION ems.fn_fill_forecast_accuracy(INT, INT) IS
'Doplní skutečné hodnoty výroby do forecast_accuracy z telemetrie. 'Doplní skutečné hodnoty výroby do forecast_accuracy z telemetrie.
learning_eligible / learning_exclude_reason: před delta_learn_min_ts (kalibrace site) se nepočítá do učení delty; learning_eligible / learning_exclude_reason: před delta_learn_min_ts (kalibrace site) se nepočítá do učení delty;
po pv_curtailment_policy_effective_from sloty s curtailment / gen cutoff / cutoff_switch_log (export off) mají NULL actual a jsou vyloučeny z učení. po pv_curtailment_policy_effective_from sloty s curtailment / gen cutoff / cutoff_switch_log (export off) mají NULL actual a jsou vyloučeny z učení;
telemetrie: is_export_limited nebo pv_derating_flags <> 0 v okně slotu → stejné vyloučení (telemetry_derating).
Volat každých 15 minut (spolu s audit_filler) pro inkrementální plnění. Volat každých 15 minut (spolu s audit_filler) pro inkrementální plnění.
p_lookback_hours: kolik hodin zpět zpracovat (default 48h pro catch-up). p_lookback_hours: kolik hodin zpět zpracovat (default 48h pro catch-up).
Pro první backfill: SELECT ems.fn_fill_forecast_accuracy(2, 8760) -- 1 rok'; Pro první backfill: SELECT ems.fn_fill_forecast_accuracy(2, 8760) -- 1 rok';

View File

@@ -234,6 +234,12 @@ as $fn$
), ),
'[]'::jsonb '[]'::jsonb
), ),
'pv_forecast_calibration',
(
select to_jsonb(c.*)
from ems.site_pv_forecast_calibration c
where c.site_id = p_site_id
),
'operational', 'operational',
jsonb_build_object( jsonb_build_object(
'heartbeat_last_seen', 'heartbeat_last_seen',

View File

@@ -27,7 +27,9 @@ SELECT DISTINCT ON (t.inverter_id)
t.gen_port_power_w, t.gen_port_power_w,
t.batt_charge_today_wh, t.batt_charge_today_wh,
t.batt_discharge_today_wh, t.batt_discharge_today_wh,
t.run_state t.run_state,
t.is_export_limited,
t.pv_derating_flags
FROM ems.telemetry_inverter t FROM ems.telemetry_inverter t
JOIN ems.asset_inverter inv ON inv.id = t.inverter_id JOIN ems.asset_inverter inv ON inv.id = t.inverter_id
ORDER BY t.inverter_id, t.measured_at DESC; ORDER BY t.inverter_id, t.measured_at DESC;

View File

@@ -99,6 +99,7 @@ def calculate_pv_power(
- Default je `7` dní. - Default je `7` dní.
- Endpoint `GET /api/v1/sites/{site_id}/forecast/pv?date=YYYY-MM-DD` vrací vždy poslední `ok` run per `(interval_start, pv_array_id)` (`DISTINCT ON`), takže UI nevidí duplikáty z historických běhů. - Endpoint `GET /api/v1/sites/{site_id}/forecast/pv?date=YYYY-MM-DD` vrací vždy poslední `ok` run per `(interval_start, pv_array_id)` (`DISTINCT ON`), takže UI nevidí duplikáty z historických běhů.
- **Kalibrace delty:** `GET /api/v1/sites/{site_id}/forecast/pv-delta-profile?from=…&to=…` vrací JSON z `ems.fn_pv_forecast_delta_profile` (`deltas`, `deltas_by_array`, `delta_learn_min_ts` z `ems.site_pv_forecast_calibration`). Volitelné query parametry: `half_life_days`, `threshold_w`, `top_n_days`, `non_top_day_factor`, `day_weight_gamma` (NULL u numerických přepsání = hodnota z kalibrační tabulky / default funkce). - **Kalibrace delty:** `GET /api/v1/sites/{site_id}/forecast/pv-delta-profile?from=…&to=…` vrací JSON z `ems.fn_pv_forecast_delta_profile` (`deltas`, `deltas_by_array`, `delta_learn_min_ts` z `ems.site_pv_forecast_calibration`). Volitelné query parametry: `half_life_days`, `threshold_w`, `top_n_days`, `non_top_day_factor`, `day_weight_gamma` (NULL u numerických přepsání = hodnota z kalibrační tabulky / default funkce).
- **Úprava kalibrace z API:** `PATCH /api/v1/sites/{site_id}/configuration/pv-forecast-calibration` s JSON tělem (částečný update); odpověď je aktuální řádek kalibrace. Souhrn konfigurace v `GET …/configuration` obsahuje klíč `pv_forecast_calibration`.
--- ---

View File

@@ -27,7 +27,7 @@
- horní mez `grid_import` zahrnuje `load_baseline_w` + nabíjení/EV/TČ (bez nekonečného importu). - horní mez `grid_import` zahrnuje `load_baseline_w` + nabíjení/EV/TČ (bez nekonečného importu).
- **Uložené vstupy plánu** (`planning_interval`): `load_baseline_w`, `pv_*_forecast_raw_w`, `pv_*_forecast_solver_w` pro UI a audit. - **Uložené vstupy plánu** (`planning_interval`): `load_baseline_w`, `pv_*_forecast_raw_w`, `pv_*_forecast_solver_w` pro UI a audit.
- **Více FVE polí s různou orientací:** `planning_engine._load_slots` sčítá predikovaný výkon za 15min přes **všechna** `asset_pv_array` dané lokality — `pv_a_forecast_w` = součet řádků s `controllable = true`, `pv_b_forecast_w` = součet s `controllable = false`. Pro každé pole a slot se bere **nejnovější** `forecast_pv_run` (`ORDER BY created_at DESC`, `DISTINCT ON (pv_array_id)`). Curtailment v LP zůstává **jedno** agregované `pv_a` (součet řiditelných polí); per-string curtailment by vyžadovalo rozšíření modelu. - **Více FVE polí s různou orientací:** `planning_engine._load_slots` sčítá predikovaný výkon za 15min přes **všechna** `asset_pv_array` dané lokality — `pv_a_forecast_w` = součet řádků s `controllable = true`, `pv_b_forecast_w` = součet s `controllable = false`. Pro každé pole a slot se bere **nejnovější** `forecast_pv_run` (`ORDER BY created_at DESC`, `DISTINCT ON (pv_array_id)`). Curtailment v LP zůstává **jedno** agregované `pv_a` (součet řiditelných polí); per-string curtailment by vyžadovalo rozšíření modelu.
- **Kalibrace PV forecastu (delta profil):** tabulka `ems.site_pv_forecast_calibration` drží per `site_id` mimo jiné `delta_learn_min_ts` (dolní mez řádků z `forecast_accuracy` pro učení delty), volitelně `pv_curtailment_policy_effective_from` a přepsání parametrů (`top_n_days`, `half_life_days`, …). `ems.fn_fill_forecast_accuracy` nastavuje `learning_eligible` / `learning_exclude_reason` (sloty před cutoffem, nebo se škrcením / gen cut-off / záznamem v `ems.cutoff_switch_log` po účinnosti policy se z učení vyřadí; u škrcení zůstává `actual_power_w` NULL). `ems.fn_pv_forecast_delta_profile` vrací `deltas_by_array` i součtové `deltas`; `ems.fn_load_planning_slots_full` aplikuje stejnou **per-pole** korekci jako UI (`fn_forecast_pv_slots_range_corrected`); pokud v JSON profilu chybí `deltas_by_array`, použije se souhrnné `deltas` rozpuštěné podle podílu výkonu pole na slotu (solver má tak stále použitou korekci i bez per-pole JSON). - **Kalibrace PV forecastu (delta profil):** tabulka `ems.site_pv_forecast_calibration` drží per `site_id` mimo jiné `delta_learn_min_ts` (dolní mez řádků z `forecast_accuracy` pro učení delty), volitelně `pv_curtailment_policy_effective_from` a přepsání parametrů (`top_n_days`, `half_life_days`, …). `ems.fn_fill_forecast_accuracy` nastavuje `learning_eligible` / `learning_exclude_reason` (sloty před cutoffem, nebo se škrcením / gen cut-off / záznamem v `ems.cutoff_switch_log` po účinnosti policy se z učení vyřadí; u škrcení zůstává `actual_power_w` NULL). Telemetrie: `ems.telemetry_inverter.is_export_limited` nebo `pv_derating_flags <> 0` v okně 15min → stejné vyloučení (`telemetry_derating`). `ems.fn_pv_forecast_delta_profile` vrací `deltas_by_array` i součtové `deltas`; `ems.fn_load_planning_slots_full` aplikuje stejnou **per-pole** korekci jako UI (`fn_forecast_pv_slots_range_corrected`); pokud v JSON profilu chybí `deltas_by_array`, použije se souhrnné `deltas` rozpuštěné podle podílu výkonu pole na slotu (solver má tak stále použitou korekci i bez per-pole JSON).
Solver optimalizuje celý horizont (typicky do konce známých OTE dat, strop z `fn_planning_horizon_end`) najednou, čímž přirozeně zvládá: Solver optimalizuje celý horizont (typicky do konce známých OTE dat, strop z `fn_planning_horizon_end`) najednou, čímž přirozeně zvládá:
- pohled dopředu (ráno ví že přes poledne bude záporná cena → prodává z baterie) - pohled dopředu (ráno ví že přes poledne bude záporná cena → prodává z baterie)

View File

@@ -26,7 +26,7 @@ Shrnutí otevřených bodů z `docs/06-open-questions.md`, checklistů v modulec
| Popis | Kde | Kdo | | Popis | Kde | Kdo |
|-------|-----|-----| |-------|-----|-----|
| Telemetrické flagy derating (místo heuristiky z `planning_interval`), volitelné rozšíření API o korigovaný výkon per `pv_array_id` v grafu. | `db/routines/R__022_fn_fill_forecast_accuracy.sql`, collector | programátor | | Sloupce `telemetry_inverter.is_export_limited` / `pv_derating_flags` jsou v DB (V058) a `fn_fill_forecast_accuracy` je respektuje; **doplnit zápis z collectoru** (režim / reg 145/179). Volitelné rozšíření API o korigovaný výkon per `pv_array_id` v grafu. | `telemetry_collector`, `db/routines/R__022_fn_fill_forecast_accuracy.sql` | programátor |
--- ---

View File

@@ -1,7 +1,10 @@
import axios, { type AxiosInstance } from 'axios' import axios, { type AxiosInstance } from 'axios'
import type { FullStatusResponse } from '../types/fullStatus' import type { FullStatusResponse } from '../types/fullStatus'
import type { SiteConfigurationResponse } from '../types/siteConfiguration' import type {
SiteConfigurationResponse,
SitePvForecastCalibrationRow,
} from '../types/siteConfiguration'
import type { Notification } from '../types/dashboard' import type { Notification } from '../types/dashboard'
import type { CurrentPlanResponse, RunPlanResponse } from '../types/plan' import type { CurrentPlanResponse, RunPlanResponse } from '../types/plan'
@@ -59,6 +62,28 @@ export async function getSiteConfiguration(siteId: number): Promise<SiteConfigur
return data return data
} }
/** PATCH /sites/{id}/configuration/pv-forecast-calibration — pouze uvedená pole. */
export type PvForecastCalibrationPatchPayload = {
delta_learn_min_ts?: string
pv_curtailment_policy_effective_from?: string | null
top_n_days?: number | null
non_top_day_factor?: number | null
day_weight_gamma?: number | null
half_life_days?: number | null
threshold_w?: number | null
}
export async function patchPvForecastCalibration(
siteId: number,
payload: PvForecastCalibrationPatchPayload,
): Promise<SitePvForecastCalibrationRow> {
const { data } = await client.patch<SitePvForecastCalibrationRow>(
`/sites/${siteId}/configuration/pv-forecast-calibration`,
payload,
)
return data as SitePvForecastCalibrationRow
}
export type SiteNotificationsResponse = { export type SiteNotificationsResponse = {
notifications: Notification[] notifications: Notification[]
} }

View File

@@ -32,6 +32,19 @@ export type SiteConfigurationOperational = {
active_plan_created_at: string | null active_plan_created_at: string | null
} }
/** Řádek `ems.site_pv_forecast_calibration` (GET /configuration → `pv_forecast_calibration`). */
export type SitePvForecastCalibrationRow = {
site_id: number
delta_learn_min_ts: string
pv_curtailment_policy_effective_from?: string | null
top_n_days?: number | null
non_top_day_factor?: number | string | null
day_weight_gamma?: number | string | null
half_life_days?: number | string | null
threshold_w?: number | null
updated_at?: string
}
export type SiteConfigurationResponse = { export type SiteConfigurationResponse = {
site: SiteConfigurationSite site: SiteConfigurationSite
grid_connection: Record<string, unknown> | null grid_connection: Record<string, unknown> | null
@@ -46,5 +59,7 @@ export type SiteConfigurationResponse = {
heat_pumps: Record<string, unknown>[] heat_pumps: Record<string, unknown>[]
operating_mode: Record<string, unknown> | null operating_mode: Record<string, unknown> | null
active_overrides: Record<string, unknown>[] active_overrides: Record<string, unknown>[]
/** Kalibrace PV delty; null pokud v DB chybí řádek (měl by existovat po V057). */
pv_forecast_calibration?: SitePvForecastCalibrationRow | null
operational: SiteConfigurationOperational operational: SiteConfigurationOperational
} }