Files
ems/docs/04-modules/forecast.md
Dusan Vojacek 5ca5eab1d8
Some checks failed
CI and deploy / migration-check (push) Failing after 14s
CI and deploy / deploy (push) Has been skipped
sync reference days
2026-05-02 14:05:09 +02:00

227 lines
11 KiB
Markdown
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.
# Modul: Forecast (Predikce výroby FVE)
## Co modul dělá
- Stahuje meteorologická data (irradiance, teplota) pro každé FVE pole zvlášť
- Vypočítává predikovaný výkon v 15min intervalech
- Ukládá výsledek per `pv_array_id` + `run_id`
- Predikce se spouští denně a před každým plánovacím během
---
## FVE pole na první instalaci (home-01)
| Pole | Výkon | Azimut | Sklon | Střídač | Řízení |
|---|---|---|---|---|---|
| A | 10 kWp | TBD | TBD | Deye 20kW | řídíme |
| B | 10 kWp | TBD | TBD | Ongridový | autonomní, **nepredikujeme odděleně** |
> **Předpoklad:** Pole B (ongridový) je zapojeno do GEN portu Deye. Jeho výkon se projeví v `pv_power_w` telemetrie jako součást celkového výkonu. Pro plánování modelujeme jen pole A. Pole B bereme jako šum / bonus který se projeví v auditu.
> Azimuty a sklony je nutné doplnit při konfiguraci lokality do `asset_pv_array`.
---
## Zdroj meteorologických dat
**Primární: Open-Meteo (open-meteo.com)**
- Zdarma pro nekomerční použití, API bez registrace
- Poskytuje GHI (Global Horizontal Irradiance), DNI, teplotu, oblačnost
- Historická data + forecast na 716 dní dopředu
- 15min granularita nativně ✓
**Endpoint:**
```
GET https://api.open-meteo.com/v1/forecast
?latitude={lat}
&longitude={lon}
&hourly=shortwave_radiation,temperature_2m
&minutely_15=shortwave_radiation,temperature_2m
&timezone=Europe/Prague
&forecast_days=3
```
**Záložní / budoucí: Solcast**
- Přesnější pro FVE, ale placený
- Podporuje per-array predikci s azimutem a sklonem přímo
- Zatím neimplementujeme, architektura to umožňuje přes `forecast_source`
---
## Výpočet výkonu z irradiance
Jednoduchý fyzikální model (dostatečný pro plánování):
```python
def calculate_pv_power(
irradiance_wm2: float, # GHI ze weather service
temp_c: float,
nominal_power_wp: int,
azimuth_deg: float,
tilt_deg: float,
shading_factor: float = 1.0,
temp_coeff: float = -0.004 # typicky -0.4%/°C pro křemík
) -> int:
# 1. Korekce na teplotu panelu
panel_temp = temp_c + 25 # zjednodušený NOCT model
temp_correction = 1 + temp_coeff * (panel_temp - 25)
# 2. Korekce na azimut a sklon (zjednodušená, bez přesného GHI→POA)
# Přesnější model: pvlib knihovna (doporučeno pro produkci)
orientation_factor = cos_angle_of_incidence(azimuth_deg, tilt_deg)
# 3. Výsledný výkon
power_w = (irradiance_wm2 / 1000) * nominal_power_wp * temp_correction * orientation_factor * shading_factor
return max(0, int(power_w))
```
> **Doporučení pro implementaci:** Použít knihovnu `pvlib` (Python) pro přesný POA irradiance výpočet z GHI + azimut + sklon. Je to standardní nástroj, dobře dokumentovaný.
---
## Kdo spouští predikci
**Python service: `forecast_service`**
### Kdy se spouští
| Trigger | Čas | Popis |
|---|---|---|
| Scheduled (cron) | každé 2 hodiny v `:05` | Průběžný refresh forecastu pro všechny aktivní site |
| Manual trigger | na vyžádání | `POST /api/v1/sites/{site_id}/forecast/run` |
### Implementované provozní změny (2026-03)
- Forecast horizont je konfigurovatelný přes `open_meteo_forecast_days`.
- Runtime guard: hodnota se clampuje do rozmezí `2..16`.
- 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ů.
- **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`.
- **Telemetrie pro učení delty:** `telemetry_collector` při Modbus poll čte reg. **145** a **178**; `fn_telemetry_inverter_sample` ukládá `is_export_limited` / `pv_derating_flags` (bity 1 = solar sell off, 2 = GEN/MI cut-off aktivní dle masky `(reg178 & 3) == 3`). `fn_fill_forecast_accuracy` sloty s těmito signály označí `telemetry_derating`.
---
## Logika běhu predikce
```python
def run_forecast(site_id: int, horizon_days: int = 2):
site = db.get_site(site_id)
arrays = db.get_pv_arrays(site_id, controllable=True)
for array in arrays:
# 1. Stáhnout meteorologická data
weather = open_meteo_client.fetch(
lat=site.lat, lon=site.lon,
start=today, end=today + horizon_days
)
# 2. Vytvořit forecast_pv_run
run = db.create_forecast_run(
site_id=site_id,
pv_array_id=array.id,
forecast_source="open_meteo",
horizon_start=today_00,
horizon_end=today_end + horizon_days
)
# 3. Vypočítat a uložit intervaly (15min)
intervals = []
for slot in weather.slots_15min:
power = calculate_pv_power(
irradiance_wm2=slot.shortwave_radiation,
temp_c=slot.temperature_2m,
nominal_power_wp=array.nominal_power_wp,
azimuth_deg=array.azimuth_deg,
tilt_deg=array.tilt_deg,
shading_factor=array.shading_factor
)
intervals.append(ForecastInterval(
run_id=run.id,
pv_array_id=array.id,
interval_start=slot.time,
power_w=power,
irradiance_wm2=slot.shortwave_radiation,
temp_c=slot.temperature_2m
))
db.upsert_forecast_intervals(intervals)
db.update_forecast_run_status(run.id, "ok")
```
---
## DB struktura
Viz `03-data-model.md`:
- `forecast_pv_run` každý běh predikce
- `forecast_pv_interval` 15min výsledky per pole a běh
---
## Tracking přesnosti forecastu
- **`ems.forecast_accuracy`** pro každý úspěšný `forecast_pv_run` a každý 15min slot ukládá predikovaný výkon, čas vzniku predikce, lead time (hodiny před začátkem slotu), později doplněnou skutečnost z telemetrie a odchylku (`error_w`, `error_pct`). Záznamy se **uchovávají trvale** (včetně všech historických běhů v `forecast_pv_run` / `forecast_pv_interval` ty se nemazají).
- **`ems.fn_fill_forecast_accuracy(site_id, lookback_hours)`** inkrementálně vloží nebo aktualizuje řádky z `forecast_pv_interval` + run metadata a dopočte `actual_power_w` jako průměr 1min telemetrie ve slotu (pole B: `gen_port_power_w`, pole A: `pv1_power_w` + `pv2_power_w`). Volat **každých 15 minut** (např. spolu s audit fillerem); parametr `lookback_hours` omezuje okno zpětného zpracování (např. 48 h běžně, větší hodnota pro jednorázový backfill).
- **`ems.vw_forecast_accuracy_by_lead_time`** agregace přesnosti podle bucketů lead time (06 h, …, 48 h+); noční sloty s nízkou výrobou (`actual_power_w` ≤ 100 W) se v metrikách typicky vynechávají.
- **`ems.vw_forecast_accuracy_daily`** denní součty forecast vs actual v kWh (Praha kalendářní den) a relativní odchylka dne.
- **Po 4+ týdnech dat** lze statistiky použít pro kalibraci `safety_factor` (nebo obdobných parametrů) v solveru viz plánovací modul.
---
## Operace SQL: mazání řádků PV forecastu za den (provozní výjimka)
Projekt standardně **nemá mazat** `forecast_pv_interval` / `forecast_pv_run`, aby zůstala historie pro přesnost. Když **záměrně** promāžeš den (např. před regenerací výstupu předpovědi), použij **`ems.fn_delete_forecast_pv_prague_calendar_day(p_day date, p_site_id int DEFAULT NULL)`** (`db/routines/R__086_fn_forecast_pv_prague_day_ops.sql`). Hranice dne jsou **`Europe/Prague` půlnoc** *(ne timezone lokality)*; `p_site_id NULL` = všechny lokality.
Příklad: `select * from ems.fn_delete_forecast_pv_prague_calendar_day('2026-05-02'::date, 2);`
Odstranění jde přes páry **`forecast_accuracy` → řádek `forecast_pv_interval`→ prázdné `forecast_pv_run`**, které měly jen interval v mazané množině (*stejně jako dříve skript*).
Na **bazální spotřebu** (`consumption_baseline_stats`) to nesahá → **`ems.fn_rebuild_consumption_baseline_stats`** v `R__085`.
---
## Referenční dny při učení delty („hezky svítily“ zpětně)
Profil **`ems.fn_pv_forecast_delta_profile`** se **nemerguje jako samostatný soubor** — při každém načítání (`fn_load_planning_slots_full` / API) znovu agreguje chybu z **`forecast_accuracy`** v okně (lookback/exponenta `half_life`, rank top dnů odvozený od energie a hladkosti dnů).
**„Zapošto“ k existující logice**:
1. Ověř, že máš `forecast_accuracy` pro ty dny (po skutečnosti slotů z `actual_power_w` z telemetrie) — obvykle díky `fn_fill_forecast_accuracy`.
2. Založ řádek v **`ems.site_pv_forecast_reference_day(site_id, day_local, notes)`**. **`day_local`** musí sedět na **`(interval_start AT TIME ZONE site.timezone)::date`** slovní hodiny lokality *(typicky datum v Praze jako u home-01 `Europe/Prague`)*.
3. *(Volitelně)* nastav **`site_pv_forecast_calibration.reference_day_weight_mult`** *(NULL = výchozí násobitel **3**, minimum v kódu 1).* Ostatní dny berou jako dosud jejich váhy `(top_n, non_top_day_factor, decay…)` současně — **„referenční den“ je multiplikátor navíc**, nesamostatný paralelní model.
Hromadně: **`ems.fn_pv_forecast_sync_reference_days(site_id, p_days_local date[], p_replace_existing bool default false)`** — nahrazením **true** nejdřív vymaže dřívější řádky reference pro site, pak doplní `unnest`; vrací celkový počet pinů lokality po operaci.
**Co to nedělá:** nepřepisuje zpětně uložené `forecast_pv_interval`; mění jen to, jak moc vstupuje ten den do **aktuálních** δ slotů používaných v plánění.
---
## Konfigurace (env proměnné)
```env
OPEN_METEO_API_URL=https://api.open-meteo.com/v1/forecast
OPEN_METEO_FORECAST_DAYS=7
FORECAST_MAX_AGE_HOURS=2 # plánovač odmítne starší predikci
FORECAST_RETRY_COUNT=3
```
---
## Monitoring
- Alert pokud forecast pro dnešní den + zítřek není k dispozici do 15:00
- Endpoint `GET /health/forecast?site_id=1&date=YYYY-MM-DD` → čerstvost a počet intervalů
- Log každého běhu (délka horizontu, počet intervalů, trvání, zdroj)
---
## Otevřené body
- [ ] Doplnit přesný azimut a sklon obou FVE polí při instalaci
- [ ] Rozhodnout: pvlib pro přesnější POA výpočet vs jednoduchý model doporučujeme pvlib od začátku
- [ ] Pole B (ongridový) zda vůbec modelovat nebo ignorovat v plánu a jen sledovat v auditu
- [ ] Solcast jako alternativa v budoucnu `forecast_source` to umožňuje bez DB změn