# Planning / LP — changelog Změny v plánovači (`planning_engine.py`, `R__063_fn_load_planning_slots_full.sql`) a souvisejících testech. Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověření. --- ## 2026-05-24 — Arbitráž: OTE místo hodin, export ve špičkách, FVE při sell<0 **Problém:** Plán ukazoval slabé nabíjení/vybíjení (KV1, BA81) přestože ekonomika (OTE) favorizovala opak. Ve špičkách MILP nevybíjel baterii naplno; noc BA81 držela SoC na rezervě bez exportu; záporný výkup neplnil FVE do baterie. **Změny:** | Oblast | Co | Proč | |--------|-----|------| | **R__063 — exportní maska** | Místo pevného vyloučení **00–04** na den prvního `sell<0`: slot vynechat z rozpočtu Wh jen pokud **existuje pozdější slot tentýž den** (před prvním `sell<0`) s `sell > sell_slot + degradace`. | Řídit se **OTE cenami**, ne hodinami. BA81 noc může exportovat; home-01 půlnoc se vynechá, pokud je lepší sell ráno. | | **R__063 — fixní tarif** | Discharge kandidáti: `sell > buy + degradace` (ne jen `sell > degradace`). | U BA81/KV1 export jen když je výkup nad fixním nákupem. | | **R__063 — PV vrstva A** | `allow_charge` z FVE při `sell < 0` **bez** filtru `future_sell_lookahead`; filtr „drž na večerní peak“ jen pro `sell ≥ 0`. | V záporném výkupním okně nabít z FVE (KV1 `block_export`). | | **LP — export shortfall** | Penalizace nevyužitého exportu na **`ge_bat`**, ne na `ge`; pro **všechny** `allow_discharge_export` sloty s kladnou marží (`sell > acquisition` resp. `sell > buy + degrad` u fixed). | Dříve jen `high_sell_slot` (globální max lookahead) → většina večerních slotů bez tlaku na vývoz. | | **LP — ge_bat push** | Min. ~8 kW export z baterie ve **všech** ekonomicky výhodných discharge slotech (ne jen večer/ráno seznam). | Plán má odpovídat „vylije co dá síť“ ve špičkách. | | **LP — záporný sell + block_export** | `charge_slots` rozšířeny o sloty `sell<0` s PV přebytkem; měkká penalizace `pv_charge_shortfall` (`bc_pv` vs přebytek FVE). | Postupné nabíjení / curtail místo plné FVE do baterie. | **Soubory:** `db/routines/R__063_fn_load_planning_slots_full.sql`, `backend/services/planning_engine.py`, `backend/tests/test_planning_charge_slot_selection.py`, `docs/04-modules/planning.md`. **Neměněno (záměrně):** - `reserve_soc_percent` u BA81 (**30 %**) — podlaha pro **prodej do sítě**; pod ní jen dům. Noc držela 30 % kvůli **zakázanému exportu v masce**, ne kvůli špatné rezervě. - Ranní export 5–11 před `sell<0`, večerní peak ≥17, kotva SoC — beze změny. **Ověření po deployi:** 1. Flyway repeatable `R__063` + restart backendu. 2. Rolling replan BA81 / KV1 / home-01. 3. MCP: noc BA81 — `allow_discharge_export=true` kde není lepší sell později; večer `abs(battery_setpoint_w)` řádově kW u slotů s `export_mode=BATTERY_SELL`. 4. `pytest backend/tests/test_planning_dispatch_milp.py backend/tests/test_planning_charge_slot_selection.py` --- ## 2026-05-24 (b) — Po deployi: export stále slabý (oprava #2) **Problém:** Po prvním deployi MCP stále `max_discharge ~300 W`, KV1 `allow_charge=false` při `sell<0`, 0× `BATTERY_SELL` u BA81/KV1. home-01 částečně OK (backend běží). **Příčiny z MCP:** 1. **Flyway `R__063` neaplikovaný** na DB → masky bez `allow_charge` u záporného výkupu (`ch_true=0` na celém runu KV1). 2. **Fixed marže:** `_slot_profitable_battery_export` používal `buy` v slotu (predikce 4,08 Kč) místo **`charge_acquisition`** (~3,09) → večerní export vypnutý i při `sell` 3,7. 3. **`ge_bat ≤ max_export × z_export`:** solver volil `z_export=0` → `ge_bat=0` navzdory push. 4. **Safety SoC floor** (~91 %) na ne-high-sell večerních slotech → téměř žádný export. **Opravy:** | Změna | Soubor | |--------|--------| | Explicitní `allow_charge` pro `sell<0` + `pv_surplus>0` | `R__063` | | Marže exportu: vždy `sell > acquisition + degrad` | `planning_engine._slot_profitable_battery_export` | | `ge_bat` push bez násobení `z_export`; `z_export ≥ ge_bat/max_export` | `solve_dispatch` | | Safety export floor ne na `profitable_export_ts` | `solve_dispatch` | | Tvrdé `bc_pv ≥ 0.9×pv_surplus` v `charge_slots` + `sell<0` | `solve_dispatch` | | Penalizace shortfall 40 / 25 Kč/kWh | konstanty | **Deploy checklist (povinné obojí):** ```bash # 1) SQL masky flyway migrate # nebo deploy skript s R__063 # 2) Backend docker compose build ems-api && docker compose up -d ems-api # rolling replan nebo počkat :15 ``` **Ověření v MCP:** ```sql -- musí být > 0 po novém runu KV1: select count(*) from ems.planning_run pr, jsonb_array_elements(pr.solver_params->'masks') m where pr.site_id=4 and pr.status='active' and (m->>'allow_charge')::boolean and (select effective_sell_price from ems.planning_interval pi where pi.run_id=pr.id and pi.interval_start=(m->>'slot')::timestamptz) < 0; ``` --- ## 2026-05-24 (c) — BA81: fixní tarif bez grid nabíjení **Problém:** Po deployi run **15810** — `max_chg ≈ 3275 W`, **`allow_grid_charge = 0`** na všech slotech. Noc 00–04 jen import pro dům (~100 W), žádné NT nabíjení ze sítě. HW limit BA81 je **6250 W** (`bms_max_charge_w`), ne 18 kW. **Příčina:** V `R__063` vrstva **B (grid)** běžela jen pro `purchase_pricing_mode <> 'fixed'`. BA81 má **`fixed`** → masky povolily jen **PV vrstvu A** (Wh rozpočet rozdělený přes denní FVE sloty → postupné ~3 kW). **Oprava:** Pro `fixed` + existuje arbitráž (`sell > buy + degrad`) → stejná AM/PM logika grid slotů jako u spotu, řazení podle času slotu (`slot_ord`), před `export_window_start`. **Ověření po `flyway migrate` + replan:** ```sql select count(*) filter (where (m->>'allow_grid_charge')::boolean) as grid_slots from ems.planning_run pr, jsonb_array_elements(pr.solver_params->'masks') m where pr.site_id = (select id from ems.site where code='BA81') and pr.status='active'; -- očekáváno > 0 select max(pi.battery_setpoint_w), max(pi.grid_setpoint_w) filter (where pi.grid_setpoint_w > 1000) from ems.planning_interval pi join ems.planning_run pr on pr.id = pi.run_id where pr.site_id = (select id from ems.site where code='BA81') and pr.status='active'; -- battery/grid nabíjení řádově k 6250 W v NT slotech ``` --- ## 2026-05-24 (d) — BA81: grid jen 1 slot (globální export okno) **Problém:** Run **15820** — mírné zlepšení (1× ~4,5 kW grid+bat o půlnoci), ale **00:45–05:45 `allow_charge=false`**, max nabíjení pořád ~3,3 kW z FVE. **Příčina:** `v_export_window_start` = **min přes celý horizont** (včerejší večerní sell 3,7 → čas ~22:15). Grid vrstva B řadí „před oknem“ vůči tomuto **jednomu** času → dnešní NT sloty (00–06) už jsou „po okně“ a nedostanou `allow_grid_charge`. **Oprava:** Sloupec **`export_window_start_at` per kalendářní den** (Prague); grid AM/PM i `buy_min_next_n` používají `wk.interval_start < wk.export_window_start_at`. **Deploy:** `flyway migrate` (R__063) + replan. --- ## 2026-05-24 (e) — BA81: FVE 13 kW → nabíjení jen ~3 kW (curtailment) **Problém:** Run **15826** — `pv≈13 kW`, `battery_setpoint≈3,3 kW`, **`pv_a_curtailed≈9 kW`** (08:00–08:45). `allow_charge=true`, ale solver škrtí FVE místo plného nabíjení. **Příčina:** 1. **`CURTAILMENT_PENALTY = 0,001 Kč/Wh`** vs degradace nabíjení → LP raději `ca` než `bc_pv`. 2. **`pv_charge_shortfall`** jen při `block_export_on_negative_sell` (KV1) — **BA81 má false** → žádný tlak na `bc_pv`. 3. SoC v plánu stagnuje ~52 % při záporném výkupu, zbytek jde do curtailment. **Oprava (`planning_engine.py`):** - `pv_charge_shortfall` pro **všechny** sloty `sell<0` + `allow_charge` + PV přebytek >500 W. - Penalizace **50 Kč/kWh**. - Tvrdé **`ca ≤ pv_a_forecast − bc_pv`** v okně záporného výkupu (nejdřív nabít, pak škrtit). **Deploy:** restart **backend** (SQL beze změny) + replan. --- ## Šablona pro další záznamy ```markdown ## YYYY-MM-DD — Krátký titul **Problém:** … **Změny:** … **Soubory:** … **Ověření:** … ```