# Plánování: rozpočet nabíjecích slotů (Wh × ceny × forecast) **Stav:** návrh k implementaci (2026-06) — **zatím neimplementováno** v produkčním kódu. **Účel:** nahradit tvrdé prahy typu `sell > min_sell + 0,20 → bc_pv = 0` (v58) a binární pre-neg „cushion“ (v33) jednotným **energetickým rozpočtem** ve `fn_load_planning_slots_full`, který pokryje fixní tarify (BA81, KV1), spot (home-01) i zkracující se okna `sell < 0` (zima). **Související:** | Dokument | Vztah | |----------|--------| | [`planning.md`](planning.md) | LP, masky, současné v58–v62 | | [`planning-arbitrage-accounting.md`](planning-arbitrage-accounting.md) | proč ne `sell < buy` v jednom slotu | | [`planning-neg-sell-strategy.md`](planning-neg-sell-strategy.md) | rampa, tail, pole B — cílové SoC v okně | | [`R__063_fn_load_planning_slots_full.sql`](../../db/routines/R__063_fn_load_planning_slots_full.sql) | místo implementace masek | | [`planning_engine.py`](../../backend/services/planning_engine.py) | LP jen respektuje masky + objective | **Changelog (plánované):** [`docs/planning-changelog.md`](../planning-changelog.md) — sekce *Plánováno: charge-slot-budget*. --- ## 1. Problém, který řešíme ### 1.1 Fixní tarif (BA81, KV1) — slunečný den ~60 % SoC **Symptom:** přes den nabíjení ze slunce jen na ~60–66 %, zbytek FVE jde do sítě při výkupu 2–3 Kč/kWh; nabíjení jen v 2–4 slotech u minima výkupu (~1,45 Kč). **Příčina (současný kód):** - `R__063` už počítá `v_energy_to_fill` a vrstvu A (PV) s kumulací Wh, ale u fixního tarifu řadí PV vrstvu podle **`store_score`** (future sell − sell). - **`planning_engine` v58:** tvrdě `bc_pv = 0` (a často i `bc_gi = 0`) když `sell > min(sell≥0) + 0,20` — LP **nesmí** nabíjet, i když SQL nastaví `allow_charge = true`. → Rozpor mezi maskou a LP; ekonomicky „správný“ export v 10:00 blokuje doplnění baterie, i když večerní arbitráž to nevyžaduje. ### 1.2 home-01 — velká baterie, kratší / slabší okno `sell < 0` **Symptom:** ráno export FVE před `sell < 0` i při výkupu ~2–3 Kč, zatímco v záporném okně by energie mohla být potřeba víc (krátké okno, slabší zimní FVE). **Příčina (současný kód):** - **v33:** `_pre_neg_pv_export_forecast_cushion_ok` — **binární** rozhodnutí: pokud forecast PV v celém denním okně `sell < 0` pokryje deficit do `soc_target[first_neg]` × 1,15 → **všechny** pre-neg sloty s přebytkem → export (`bc_pv = 0`, push `ge_pv`). - Neptá se: *kolik Wh musím nabít **před** oknem*, když *uvnitř* okna forecast nestačí. - **v44:** na neg den **žádný grid** před 1. `sell < 0` — při nedostatku FVE v okně chybí páka levného NT před oknem. ### 1.3 Společná chyba modelu | Špatně | Správně | |--------|---------| | Prah `sell > min + 0,20` | **Kolik Wh** chybí do cíle a **které sloty** je nejlevněji doplní | | Binární cushion OK / fail | **pre_window_wh** = max(0, deficit − in_window_wh) | | `store_score` u fixního buy | U fixního tarifu řadit **`sell ASC`** (výkup = příležitostní cena uložení) | | LP přepisuje SQL masky (v58) | LP **jen** `bc_*` kde `allow_charge`; export kde maska nepřidělila charge budget | --- ## 2. Principy návrhu 1. **SQL-first:** výběr nabíjecích slotů = **`ems.fn_load_planning_slots_full`** (rozšíření `R__063`), ne nové tvrdé větve v `solve_dispatch` kromě stávajících bezpečnostních guardů (load-first, večerní push, neg fáze). 2. **Energetický rozpočet (Wh), ne Kč prah:** ceny řadí **prioritu slotů**; zastavení kumulace je **`cum_wh ≥ target_wh`** (± `charge_slot_buffer`). 3. **Forecast v každém slotu:** `pv_surplus_w` = max(0, pv_a + pv_b − load_baseline − …) už ve work tabulce; nabíjecí příspěvek slotu = `min(pv_surplus_w, max_charge_w) × charge_eff × 0,25` (+ volitelně grid `per_slot_charge_wh`). 4. **Oddělení nákupního a výprodejního okna** — viz [`planning-arbitrage-accounting.md`](planning-arbitrage-accounting.md); `charge_acquisition` z vybraných slotů, ne `min(buy)` celého horizontu. 5. **Jeden algoritmus, více režimů:** stejná kostra pro spot i fixed; liší se **řazení** (buy vs sell), **cíl SoC** a **výjimky** (neg den, block_export). --- ## 3. Slovník | Symbol / pole | Význam | |---------------|--------| | `energy_to_fill_wh` | `soc_max_wh − current_soc_wh` (základní deficit do plné baterie) | | `charge_target_wh` | Cíl pro výběr slotů — může být `< soc_max` (rezerva neg okna, safety, večerní export) | | `in_window_wh` | Odhad energie do baterie **uvnitř** speciálního okna (např. všechny sloty `sell < 0` téhož pražského dne), z forecastu A+B (+ volitelně grid v okně) | | `pre_window_wh` | `max(0, charge_target_wh − in_window_wh × reliability_factor)` — kolik Wh je třeba doplnit **před** oknem | | `slot_charge_wh[t]` | Wh, které lze v `t` reálně natočit (PV surplus cap + výkon baterie) | | `allow_charge` | SQL maska: LP smí `bc_pv` / `bc_gi` | | `allow_grid_charge` | podmnožina: smí `bc_gi` | | `charge_slot_reason` | debug: `grid_layer_b`, `pv_layer_a`, `pre_neg_fill`, `neg_window`, `buy_negative`, … | --- ## 4. Jádro algoritmu (společné) Pro každý běh plánovače (site, horizont, `current_soc_wh`): ### Krok 1 — Cíl energie ```text charge_target_wh := min( soc_max_wh − current_soc_wh, optional_cap_from_neg_strategy, -- viz §6 optional_safety_soc_target_wh ) ``` - Výchozí: doplnit do **`soc_max_wh`** (s `charge_slot_buffer` z `asset_battery` jako dnes). - U **home-01** s neg dnem: cíl na vstupu do okna = **`soc_need[first_neg_sell]`** z rampy (v35/v36), ne fixních 80 %. - Rezerva pro okno `sell < 0`: stávající logika `v_pv_layer_cap_wh -= neg_window_pv_surplus_wh` v `R__063` zůstane jako **snížení** `charge_target_wh` před oknem, ne jako samostatný binární export. ### Krok 2 — Dodávka uvnitř speciálního okna (volitelná větev) Pro každý pražský den s alespoň jedním `sell < 0`: ```text in_window_wh := sum over slots t in neg_window(day): min(pv_surplus_w[t], max_charge_w) * charge_eff * 0.25 + (optional) grid_wh if allow_grid in neg window (v45) ``` `reliability_factor` ∈ (0, 1] — např. **0,85** zimní / krátké okno, **1,0** letní dlouhé okno; nebo odvozené z počtu slotů `sell < 0` (≤ 4 sloty → nižší faktor). ### Krok 3 — Deficit před oknem ```text pre_window_wh := max(0, charge_target_at_neg_entry − in_window_wh * reliability_factor) ``` Kde `charge_target_at_neg_entry` = SoC potřeba na `first_neg_sell` (z rampy / `soc_need`), minus `current_soc` (pozorované), přepočteno na Wh. ### Krok 4 — Fronta slotů (řazení + kumulace) Kandidáti = sloty s `slot_charge_wh > 0` **nebo** (pro grid vrstvu) sloty bez PV, kde smí síť. **Řazení (priorita):** | Režim | Primární klíč | Sekundární | |-------|----------------|------------| | Spot (`purchase_pricing_mode ≠ fixed`) | `buy_price ASC` | den plánu, před exportním oknem, `slot_ord` | | Fixní tarif | `sell_price ASC` | stejné geografické priority jako dnes v R__063 | **Kumulace:** ```text cum := 0 for slot in candidates ordered: if cum >= budget_wh: break allow_charge[slot] := true if grid_layer: allow_grid_charge[slot] := true cum += slot_charge_wh[slot] -- u grid vrstvy min(cum increment, per_slot_charge_wh) ``` **Budgety (vrstvy, po sobě):** 1. **Pre-neg / před oknem** — `budget = pre_window_wh` (jen sloty `t < first_neg_sell` téhož dne). 2. **Grid B** — `budget = grid_target_wh` (AM/PM 50/50 jako dnes); spot: nejlevnější `buy`; fixed: nejlevnější `sell` u slotů splňujících marži. 3. **PV A — zbytek** — `budget = max(0, charge_target_wh − grid_filled_wh − pre_neg_filled_wh)`. Po výběru: sloty **mimo** vybranou frontu nemusí mít `allow_charge` — LP pak může exportovat FVE, pokud objective dává smysl (bez v58 zákazu). ### Krok 5 — Export před oknem (home-01) **Nahradit** v33 binární cushion: ```text export_allowed_pre_neg[t] := t in pre_neg_calendar_window AND pv_surplus_w[t] > threshold AND NOT allow_charge[t] -- přebytek po naplnění pre_window_wh AND (optional) sell[t] >= sell_export_floor[t] -- ne dump pod ranním pásmem (R__063 morning zone) ``` V Pythonu: `pre_neg_pv_export_ts` = sloty s exportem **jen pokud** nejsou v charge frontě; případně měkká penalizace místo `bc_pv = 0` na celé pásmo. --- ## 5. Fixní tarif (BA81, KV1) ### 5.1 Změny oproti dnešku | Dnes (v58–v59) | Plán | |----------------|------| | `bc_pv = 0` if `sell > min + 0,20` | **Zrušit** v `planning_engine.py` | | PV vrstva A: `store_score DESC` | Fixed: **`sell_price ASC`** + kumulace `pv_surplus` Wh | | Grid: min sell sloty (v59) | Zachovat, sladit s jednotnou frontou (grid = vrstva B) | | Večer push: `bc_* = 0` | Zachovat (exportní okno) | ### 5.2 Ekonomická interpretace - **Buy** je konstantní → rozhoduje **výkup v slotu** (opportunity cost uložení do baterie). - Nabíjet v pořadí **nejnižší sell**, dokud `cum < charge_target_wh`. - Export v slotu s vyšším sell je OK **až poté**, co je rozpočet Wh vyčerpán v levnějších slotech (včetně pozdějších levných sellů v pořadí času — viz pořadí: nejdřív globálně nejlevnější sell v rámci dne / AM-PM, viz níže). ### 5.3 AM/PM a pořadí v čase Zachovat stávající **AM/PM rozpočet** `grid_target_wh` (50/50, přeliv AM→PM). Uvnitř segmentu: - fixed grid: `sell ASC` + filtr `sell ≤ min(sell≥0) + degrad + ε` (jako v59) **nebo** čistě kumulace Wh bez ε, pokud Wh budget stačí — **rozhodnutí při implementaci:** preferovat **Wh kumulaci**; ε jen jako failsafe proti grid nabíjení za 6 Kč v noci (KV1). ### 5.4 Večerní špička a profitable export Beze změny principu: `allow_discharge_export` + `evening_push_ts`; v push slotech **`allow_charge = false`**. --- ## 6. Spot — home-01 a negativní výkup ### 6.1 Běžný spot (bez neg dne v horizontu) Stejná kostra jako §4: - Grid vrstva: **`buy ASC`** (stávající spot loop + self-konzistentní filtr `pv_charge_wh_ahead`). - PV vrstva: **`store_score DESC`** nebo hybrid — **po** grid; budget = zbytek `charge_target_wh`. ### 6.2 Den s `sell < 0` (neg strategie) Integrace s [`planning-neg-sell-strategy.md`](planning-neg-sell-strategy.md): | Komponenta | Role v charge-slot budget | |------------|---------------------------| | `soc_need[t]` rampa (v35/v36) | `charge_target_at_neg_entry` | | `in_window_wh` z A+B v neg sloty | sníží `pre_window_wh` | | `pre_window_wh > 0` | **nabíjecí fronta před `first_neg_sell`** (PV + případně grid) | | Tail / T / curtail | **beze změny** v LP (fáze neg okna) | | v44 `neg_day_no_grid_before_neg_sell` | **Změkčit:** povolit grid v N nejlevnějších `buy` slotech před oknem, pokud `pre_window_wh > in_window_wh × factor` a `buy < 0` nebo `buy ≤ ref_buy_am + degrad` | ### 6.3 Nahrazení v33 cushion | v33 (dnes) | charge-slot budget | |------------|-------------------| | `cushion_ok` → export vše pre-neg | `pre_window_wh` malé → více `allow_charge` pre-neg | | `cushion_fail` → nabíjet | `pre_window_wh` velké → fronta nabíjení | | `bc_pv = 0` v celém `pre_neg_pv_export_ts` | Jen sloty, kde **není** `allow_charge` a export je ekonomicky výhodný | **Zima / krátké okno:** málo slotů `sell < 0` → malé `in_window_wh` → velké `pre_window_wh` → **více nabíjení před oknem**, i při sell 2–3 Kč, pokud jsou to nejlevnější dostupné sloty (ne plošný export). ### 6.4 Velká baterie (64 kWh) - `charge_target_wh` může být **desítky kWh** — fronta musí počítat **skutečné** `slot_charge_wh`, ne jen cap 6 slotů / segment, pokud budget vyžaduje více (rozšířit `grid_charge_cap_*` odvozeně od `ceil(budget / per_slot_charge_wh)` — částečně už je). - `charge_acquisition` = vážený buy ve vybraných `allow_grid_charge` slotech (stávající two-pass v Pythonu). --- ## 7. Role LP po změně masek ` solve_dispatch` **nesmí** přepisovat energetický výběr prahy typu v58. | Oblast | LP chování | |--------|------------| | Nabíjení | `bc_pv`, `bc_gi` jen kde `allow_charge` / `allow_grid_charge` | | Export FVE | objective `−ge_pv×sell`; **bez** `bc_pv=0` jen kvůli sell | | Export bat | `allow_discharge_export`, `charge_acquisition`, večerní push | | Guard | `ge_pv=0` if `sell < charge_acquisition − degrad` (spot) — **měkká** hranice hodnoty uložené energie | | Spot grid | v61: `bc_gi=0` if `buy > charge_acquisition + degrad` | | Neg fáze | rampa, tail, T — beze změny | --- ## 8. Návrh rozšíření `fn_load_planning_slots_full` Nové / rozšířené sloupce ve výstupu (nebo JSON v meta — preferováno sloupce pro debug): | Sloupec | Typ | Popis | |---------|-----|--------| | `charge_budget_wh` | numeric | celkový cíl pro tento běh | | `charge_slot_wh` | numeric | odhad Wh v daném slotu | | `charge_cum_wh` | numeric | kumulativa po řazení (audit) | | `charge_layer` | text | `pre_neg` / `grid_am` / `grid_pm` / `pv_a` / … | | `pre_window_wh` | numeric | jen informativní per běh (nebo per den v `solver_params`) | V `planning_run.solver_params` (commit meta): ```json { "charge_slot_budget": { "charge_target_wh": 42000, "pre_window_wh_by_day": {"2026-06-02": 18000}, "in_window_wh_by_day": {"2026-06-02": 12000}, "reliability_factor": 0.85, "planner_build_tag": "…-charge-slot-budget-v1" } } ``` --- ## 9. Co zrušit / neimplementovat znovu | Položka | Akce | |---------|------| | v58 `fixed_high_sell_no_pv_charge` | **odstranit** po nasazení budget | | v58 `FIXED_PV_CHARGE_ONLY_NEAR_MIN_SELL_CZK_KWH` | **odstranit** (konstanta) | | v59 `fixed_grid_charge_unprofitable` část `sell > min + 0,20` | nahradit: grid jen ve vybrané frontě; případně ponechat `sell < buy` guard pro grid | | v60 `sell < buy` → `bc_gi=0` (spot) | **neobnovovat** (záměrně zrušeno v61) | | v33 binární cushion jako hlavní páka | nahradit §4 krok 2–3; cushion ponechat jako **audit** / `solver_params.inputs.pre_neg_cushion_legacy_ok` | --- ## 10. Fáze implementace (doporučené pořadí) 1. **Dokumentace + testy scénářů** (tento soubor, pytest fixtures s umělými sloty). 2. **`R__063`:** fixed PV vrstva `sell ASC` + Wh kumulace; sloupce debug; bez změny Pythonu → ověřit, že v58 stále blokuje (regrese). 3. **Python:** odstranit v58/v59 `bc_pv`/`bc_gi` fixed větve; spoléhat na masky. 4. **`R__063` + Python:** pre-neg `pre_window_wh` pro spot; zúžit `pre_neg_pv_export_ts`. 5. **v44 změkčení:** grid před neg jen když `pre_window_wh` > práh. 6. **Dokumentace + changelog** tag `charge-slot-budget-v1`; MCP ověření home-01 / KV1 / BA81. --- ## 11. Ověření ### 11.1 Automatické testy (pytest) | Scénář | Očekávání | |--------|-----------| | Fixed, slunečný den, nízký ranní SoC | `allow_charge` v mnoha PV slotech; max SoC v plánu → blízko `soc_max` | | Fixed, min sell odpoledne | dřívější sloty s vyšším sell **bez** `allow_charge`, pokud budget vyčerpán v levnějších | | Spot, krátké neg okno (2–4 sloty), slabá FVE | `pre_window_wh > 0`; nabíjení před `first_neg_sell`; **ne** plošný pre-neg export | | Spot, dlouhé neg okno, silná FVE | po naplnění `pre_window_wh` může export pre-neg v drahých sell | | home-01 velká baterie | kumulace ≥ desítky kWh přes více slotů | ### 11.2 SQL / MCP ```sql select interval_start, allow_charge, allow_grid_charge, charge_layer, charge_slot_wh, charge_cum_wh, sell_price, pv_surplus_w from ems.fn_load_planning_slots_full(, , , ) where allow_charge order by interval_start; ``` Po deployi: aktivní `planning_run.solver_params->'charge_slot_budget'`; u home-01 `pre_neg_pv_export_slots` ⊆ sloty bez `allow_charge`. ### 11.3 Regrese - Večerní push (v57), spot v61, KV1 noc v62 — **nesmí** rozpadnout. - Neg rampa v35/v36 — SoC v okně `sell < 0` stejné cíle, mění se jen **příprava před oknem**. --- ## 12. Otevřené body (rozhodnutí před kódem) 1. **`reliability_factor`:** fixní 0,85 vs funkce počtu neg slotů? 2. **Fixed řazení:** globální `sell ASC` přes den vs AM/PM segmenty (doporučení: AM/PM budget + `sell ASC` v segmentu). 3. **Večer před neg dnem:** zda `charge_target_wh` zahrnuje večerní výboj D−1 (`neg_evening_before_neg`) — ano, přes `soc_need`, ne duplicitní budget. 4. **Multi-day horizont:** každý pražský den vlastní `pre_window_wh` (jako v36 bundle) — **ano**. 5. **UI:** badge „charge budget“ ve frontendu — volitelné, až budou sloupce v API. --- ## 13. Shrnutí pro produkt Jednou větou: **místo prahu „sell nad minimum + 20 haléřů“ spočítáme, kolik Wh chybí do cíle, kolik jich dodá záporné výkupní okno z forecastu, a zbytek nabijeme v nejlevnějších slotech (buy nebo sell podle tarifu) s ohledem na PV přebytek a spotřebu — u home-01 tím nahradíme plošný ranní export před `sell < 0`, u KV1/BA81 doplníme baterii ve slunečný den nad ~60 %.**