# 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í každé 2 hodiny v `:05` a ručně přes API. Plánovač používá
  poslední dostupné uložené forecasty; forecast nespouští implicitně před každým
  plánovacím během.

## Kanonický forecast pro plánování (single source of truth)

Pro plánování (solver) a UI tabulky slotů je **kanonický** výkon FVE počítaný v DB funkcí:

- `ems.fn_forecast_pv_slots_range_canonical_ab(...)`

Ta kombinuje dvě korekce do jedné řady:

- **delta-korekci** per `pv_array_id` (z `ems.fn_pv_forecast_delta_profile`)
- **rolling multiplikativní faktor** vs telemetrie (z `ems.fn_pv_forecast_correction_factor`) s lineárním **decay** do 1.0

Výstup je rozdělený na **PV‑A** (`controllable=true`, curtailment v LP) a **PV‑B** (`controllable=false`).

---

## FVE pole na první instalaci (home-01)

| Pole | Výkon | Azimut | Sklon | Střídač | Řízení |
|---|---|---|---|---|---|
| A | 10 kWp | 184° | 22° | Deye 20kW | řídíme |
| B | 10 kWp | 184° | 35° | Ongridový | autonomní, predikujeme jako samostatné pole |

> **Aktuální implementace:** Forecast služba počítá všechna FVE pole lokality,
> která mají vyplněný `azimuth_deg` a `tilt_deg`; plánovač pracuje odděleně s
> `pv_a_forecast_w` i `pv_b_forecast_w`.

> Azimut je uložen v kompasové / pvlib konvenci: `0=N`, `90=E`, `180=S`,
> `270=W`.

---

## 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 7–16 dní dopředu
- 15min granularita nativně ✓

**Endpoint:**
```
GET https://api.open-meteo.com/v1/forecast
  ?latitude={lat}
  &longitude={lon}
  &minutely_15=direct_normal_irradiance,diffuse_radiation,shortwave_radiation,temperature_2m
  &timezone=auto
  &forecast_days=7
```

**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

Implementace používá `pvlib` a model POA irradiance `haydavies`:

```python
poa_global = pvlib.irradiance.get_total_irradiance(
    surface_tilt=tilt_deg,
    surface_azimuth=azimuth_deg,  # 0=N, 90=E, 180=S, 270=W
    solar_zenith=solar_pos["apparent_zenith"],
    solar_azimuth=solar_pos["azimuth"],
    dni=dni,
    ghi=ghi,
    dhi=dhi,
    dni_extra=dni_extra,
    model="haydavies",
)["poa_global"].fillna(0).clip(lower=0)

area_m2 = nominal_power_wp / (1000.0 * 0.20)
power_w = (poa_global * area_m2 * 0.20 * shading_factor).clip(
    lower=0,
    upper=nominal_power_wp * 1.1,
)
```

---

## 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).
- **Cache delty (V079):** sloupce `delta_profile_cache` / `delta_profile_cached_at` v `site_pv_forecast_calibration`; refresh `ems.fn_refresh_site_pv_delta_profile_cache(site_id)` po `fn_fill_forecast_accuracy` a po PATCH kalibrace; čtení pro plánování/UI přes `fn_pv_forecast_delta_profile_cached` (TTL 30 min, pak fallback na plný přepočet).
- **Ú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):
    site = db.get_site(site_id)
    arrays = db.get_pv_arrays_with_azimuth_and_tilt(site_id)

    for array in arrays:
        # 1. Stáhnout meteorologická data
        weather = open_meteo_client.fetch(
            lat=site.lat, lon=site.lon,
            forecast_days=clamp(OPEN_METEO_FORECAST_DAYS, 2, 16)
        )

        # 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.insert_forecast_intervals(intervals)
```

---

## 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 (0–6 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
```

---

## Monitoring

- Zatím není samostatný `/health/forecast` endpoint.
- Stav se kontroluje přes logy běhu `scheduled_forecast_refresh`, přes forecast API
  a přes obecné health endpointy.
- Log každého běhu (délka horizontu, počet intervalů, trvání, zdroj)

---

## Otevřené body

- [ ] Ověřit přesný azimut a sklon obou FVE polí proti skutečné instalaci
- [ ] Solcast jako alternativa v budoucnu – `forecast_source` to umožňuje bez DB změn
