dalsi uprava vypoctu delty (ignorujeme orezane vyroby)
This commit is contained in:
@@ -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). |
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
12
db/migration/V058__telemetry_inverter_derating_flags.sql
Normal file
12
db/migration/V058__telemetry_inverter_derating_flags.sql
Normal 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.';
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user