# 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-27 (e) — pre-`buy<0` discharge_export window + SoC cap (v15) **Problém (home-01 run 16636, tag v14):** SoC cap 47 % spočítán správně (`pre_neg_buy_soc_cap_wh=29800`), ale LP **necilí** — 12:45 SoC = 98,4 %, slack 33 kWh × penalty 5 Kč/kWh = 165 Kč LP přijímá. Důvod: startovní SoC 58 % > cap 47 %, a R__063 v noci nepovoluje `allow_discharge_export=true` → **LP nemá cestu jak baterii vybít** kromě malé self-consumption. Tvrdý `bc_pv[t] = 0` mimo charge_slots by vyřešil PV pumping, ale rozbije bilanci v testech kde `pv_surplus > 0 + sell < pv_store_val` (export blokován v Pythonu). **Oprava (tag `2026-05-27-hard-pv-mask-v15`):** - **Cap snížen na `min_soc + 0.15 × usable`** (~25 % SoC) místo dynamického z `neg_window_capacity`. - **Penalty zvýšena 10×** na 0,05 Kč/Wh (= 50 Kč/kWh), nad marginal arbitrage (~5 Kč/kWh). - **Když start SoC > cap_raw**, rozšíříme `discharge_export_slots` o sloty před `first_neg_buy_idx` se `sell ≥ 0.8 × max(sell_pre_neg_buy)`. LP tak může vybít baterii **přes ge_bat** v noci/ráno za sell ~3 Kč/kWh; pak v buy<0 okně nasáje z OTE záporných cen. - Když start ≤ cap_raw, cap = current_soc (constraint „nepřibývat" místo „klesnout" → triviálně splněno, LP nikdy nedostane infeasible kvůli rezervaci). **Ekonomická logika home-01 zítra:** - Vybít 20 kWh v noci za sell~3 Kč/kWh = ~60 Kč - Nabít 20 kWh v buy<0 okně za buy~−0.22 Kč/kWh = ~+4 Kč (acquisition záporný) - Vyprodat v peak za sell~4.4 Kč/kWh = ~88 Kč - Total alternative: ~152 Kč (vs current bez vybíjení ~132 Kč) **Ověření:** v `solver_params.inputs` nově: `first_neg_buy_idx`, `pre_neg_buy_soc_cap_wh`, `pre_neg_buy_soc_slack_wh`. Replan home-01 → SoC v 12:45 ≤ 25 % (nebo cap = start SoC, pokud bottleneck export). `bat_setpoint_w < 0` v některých nočních / ranních slotech s peak sell. --- ## 2026-05-27 (d) — pre-`buy<0` SoC cap v LP (v14) **Problém (home-01 run 16622, tag v13):** Fix 1 (acquisition ≥ 0) i Fix 2 (R__063 `v_pv_layer_cap_wh` redukce) byly nasazené, ale plán pro 2026-05-25 zůstal stejný: 10:30 SoC = 95 %, 11:00 SoC = 98,3 %, 13:00–14:45 (buy<0, sell<0) baterka plná → export pole A do mínusu + curtail 5 kW pole A. Příčina: - LP v `solve_dispatch` má **bc_pv[t] ≤ pv_surplus_w** i pro sloty `t not in charge_slots` (Python obchází tvrdou masku R__063 pro PV charging) → R__063 Fix 2 (vrstva A cap = 0) nemá efekt. - `acquisition` je v LP **vstupní konstanta**, ne endogenní funkce slotů → LP nevidí per-slot opportunity cost (nabíjení v `buy<0` = záporná cena vs PV = 0) → LP rovnoměrně nabíjí kdekoliv má PV. - Marginal arbitrage = peak_sell (4,40) − avg_neg_buy (−0,22) = **4,62 Kč/kWh**, vs PV→bat (acquisition 0,757) = 3,64 Kč/kWh — rozdíl ~1 Kč/kWh × 32 kWh denně. **Oprava (tag `2026-05-27-pre-neg-buy-soc-cap-v14`):** Tvrdý LP constraint na SoC v posledním slotu před prvním `buy<0`: - `neg_buy_indices = [t for t,s in slots if buy<0]` - `neg_window_capacity_wh = Σ min(max_charge_w, pv_surplus + grid_max_import) × 0.25 × eff` přes neg_buy sloty. - `pre_neg_buy_soc_cap_wh = max(min_soc_wh, soc_max_wh − min(neg_window, 0.9 × usable))`. - LP: `soc[first_neg_buy_idx − 1] ≤ pre_neg_buy_soc_cap_wh + slack`, slack penalizován 0,005 Kč/Wh (= 5 Kč/kWh, lehce nad marginal arb → LP cap dodrží, ale neinfeasible při krátkém horizontu / vysokém startovním SoC). **Důsledek:** LP musí baterii do `buy<0` okna dorazit s volnou kapacitou — místo ranního PV nabíjení (sell≥0 sloty) export pole B (green bonus 7,135 Kč/kWh) a curtail pole A; v `buy<0` okně max import + max PV → baterka plná; večer max export. **Ověření:** v `solver_params.inputs` nově: `first_neg_buy_idx`, `pre_neg_buy_soc_cap_wh`, `pre_neg_buy_soc_slack_wh`. Replan home-01 zítra (2026-05-25) → SoC v 12:45 ≤ cap (cca 10–15 %), 13:00–14:45 SoC stoupá z capu k 100 %, `pv_a_curtailed_w` v okně blíží 0. --- ## 2026-05-27 (c) — rezervace SoC pro `sell<0` okno + fallback acquisition ≥ 0 (v13) **Problém (home-01 run 16614, tag v12):** Aktivní plán pro 2026-05-25: - 10:30 SoC = 96,9 %, 10:45 SoC = 98,3 % (baterie plná z PV ráno) → odpoledne v `sell<0` slotech (13:00–14:45, sell až −1,08 Kč) **ge_pv export** + curtail pole A 5 kW. Ztráta 6+ Kč. - `acquisition_pass1 = −0,035` (`R__063` fallback path: `(ref_buy_am + ref_buy_pm)/2`, ref_buy_pm < 0 protože PM zahrnuje 13:30–14:00 s buy ≈ −0,36 Kč) → `two_pass_converged = false`. **Oprava (tag `2026-05-27-neg-sell-soc-reservation-v13`):** - **`R__063` PV vrstva A — rezervace pro `sell<0` okno:** před iterátorem vrstvy A spočítat `v_neg_window_pv_surplus_wh = sum(min(pv_surplus_w, max_charge_w) * eff * 0.25) FILTER (sell<0, pv_surplus>0)`. Snížit `v_pv_layer_cap_wh` o tuto hodnotu (lower bound 0). Důsledek: před `sell<0` oknem se nabíjí jen `deficit − neg_window_pv_wh`; do okna doráží baterie nenaplněná a `sell<0` PV slot ji dorovná místo exportu / curtailu pole A. - **`R__063` fallback acquisition:** když `v_est_grid_wh = 0` a `min(buy) FILTER (allow_grid_charge AND buy>=0)` je NULL, místo avg `ref_buy_am/pm` (může být záporný) použít `coalesce(min(buy) FILTER (buy>=0), 0)`. Navíc `v_charge_acquisition := greatest(v_charge_acquisition, 0)` jako pojistka — arbitrážní akviziční cena nesmí být < 0. **Ověření:** - Replan home-01 (po redeploy R__063) → 10:45 SoC < 95 %, 13:00–14:45 SoC roste (PV charging), 13:30 `grid_setpoint` < 0 jen pole B (curtail pole A = 0), bilance: `cashflow_czk(13:00–15:00) > 0`. - `acquisition_pass1_czk_kwh ≥ 0`, `two_pass_converged = true`. --- ## 2026-05-27 (b) — acquisition: vyloučit záporný OTE buy z váženého průměru **Problém (home-01 run 16588):** `two_pass_converged=false`, `acquisition_pass1≈−0.035` (pass1 nabíjení v `buy<0` slotech), `pass2≈0.88`. Noční grid 4,8 Kč už v plánu není (maska B OK), ale two-pass a arbitrážní marže exportu baterie byly křivé. **Oprava:** `R__063` — vážená acquisition ve filtru B a v `charge_acquisition_buy_czk_kwh` jen z `allow_grid_charge` s `buy_price >= 0`. `planning_engine._recompute_charge_acquisition_from_results` přeskočí `buy<0`. **Ověření:** po redeploy replan home-01 → `two_pass_converged=true`, `|acq1−acq2| < 0.05`. --- ## 2026-05-27 — self-konzistentní grid maska B + ekonomický rozpad plánu (v12) **Problém (home-01, run 16522, tag v11):** Noční grid nabíjení (23:30–23:45, buy ~4,8 Kč) při `acquisition_pass1≈4,81` / `pass2≈0,84`, `two_pass_converged=false`; 26 slotů export při `sell<0`. **Oprava (tag `2026-05-27-self-consistent-grid-mask-v12`):** - **`R__063`:** iterativní filtr vrstvy B (spot) + sloupce `pv_charge_wh_ahead`, `neg_buy_wh_ahead`, `grid_charge_suppressed_reason`, `min_buy_before_cutoff_czk_kwh`; failsafe unlock. - **`V081`:** `planning_interval.cashflow_czk`, `battery_arbitrage_czk`, `penalty_czk`, `green_bonus_czk`; commit přes `fn_planning_run_commit`. - **`planning_engine.py`:** post-processing ekonomiky, `solver_params.objective_terms` rozšíření; `fn_plan_explain_bundle` → `economics_summary`. **Ověření:** `pytest backend/tests/test_planning_economics_columns.py`, `DynamicGridFilterTests`, `Home01RegressionTests::test_home01_no_night_charge_before_pv_day`, `test_two_pass_converged_after_filter`; po deploy MCP: `grid_charge_suppressed_reason` ve `fn_load_planning_slots_full`, `two_pass_converged=true` na novém run. --- ## 2026-05-26 (o) — home-01: neg. výkup bez placeného exportu FVE + dump baterie před extrémním buy **Problém (run 16480, tag v10):** Po ranním nabití na `soc_max` solver při `sell<0` exportoval **celý PV přebytek** (~9 kW, `PV_SURPLUS`) — binárka `w_pv_full_neg` povolila `ge_pv ≤ pv_surplus` místo jen ventilu pole B. Zároveň `ge_bat=0` blokoval výboj baterie před oknem `buy ≤ −2` (round-trip arbitráž). **Oprava (tag `2026-05-26-neg-sell-bat-dump-extreme-buy-v11`):** - Spot `sell<0`: `ge_pv=0` dokud není plná baterie; při plné jen `ge_pv ≤ pv_b` (`w_pv_b_vent_neg`) + penalizace `NEG_SELL_PV_B_VENT_PENALTY` (4 Kč/kWh). - Před extrémním buy (`buy ≤ planner_extreme_buy_threshold`, default −2): v okně **12 slotů** smí `ge_bat>0` při `sell<0`, pokud `min_buy_future < sell − degrad`. - Odstraněn `w_pv_full_neg` (export celého surplusu). **Ověření:** `test_neg_sell_full_battery_exports_at_most_pv_b_not_full_surplus`, `test_neg_sell_bat_dump_before_extreme_buy`, `test_neg_sell_pv_to_battery_not_grid_when_soc_has_room`; po deploy replan home-01 — neg sell bez ~9 kW exportu. --- ## 2026-05-25 (n) — home-01 AUTO: záporný výkup bez exportu, večerní špička **Problém (run 16412, AUTO):** Dnes večer téměř bez exportu (terminal SoC drží energii na zítřek); zítra 07:30+ masivní **PV_SURPLUS** při `sell<0` místo nabíjení; zítra večer export OK. **Příčiny:** 1. Spot při `sell<0`: `skip_pv_store_block` kvůli `pv_b` povoloval export i s prázdnou baterií. 2. R__063 večerní maska spot: `sell > ref_buy` — ve slotu často `sell < buy`, večerní export dnes vypnutý. 3. Večerní `ge_bat` push jen 50 % výkonu vs. terminal SoC shadow. **Oprava (tag `2026-05-25-home01-neg-sell-evening-v10`):** - Spot `sell<0`: `ge_pv`/`ge` jen pokud `soc[t-1] ≥ soc_max − headroom` (binárka `w_pv_full_neg`). - R__063: večerní peak u spotu jako u fixního tarifu (denní max výkupu). - `PEAK_EXPORT_SHORTFALL` 80 Kč/kWh; večerní push na plný `EVENING_BATTERY_EXPORT_MIN_W`. **Ověření:** `Home01RegressionTests::test_neg_sell_pv_to_battery_not_grid_when_soc_has_room`; po deploy replan home-01 — neg sell bez exportu při SoC < max. --- ## 2026-05-25 (m) — BA81: záporný výkup bez exportu podle DB `purchase_pricing_mode` **Problém (tag v8 v produkci):** KV1 OK; **BA81** pořád export při `sell < 0` (dnes i zítra). v8 používalo `_horizon_fixed_tariff_like` (rozptyl **buy** < 0,25 Kč/kWh). U BA81 buy skáče **NT/VT** (3,09 ↔ 4,09) → heuristika **false** → zákaz exportu se neaplikoval. **Oprava (tag `2026-05-25-purchase-fixed-neg-sell-v9`):** - `ems.fn_planning_site_context` vrací **`market.purchase_pricing_mode`** / **`sale_pricing_mode`** z `site_market_config`. - Při **`sell < 0`** a **`purchase_pricing_mode = fixed`**: `ge = 0` (nezávisle na rozptylu buy). **home-01** (spot nákup) výjimku nemá — může ventovat PV B. - `_horizon_fixed_tariff_like` zůstává jen pro **drahý import** / `charge_acquisition` (heuristika + DB `fixed`). **Ověření:** `pytest …::NegativeSellPvChargeTests::test_ba81_fixed_purchase_nt_vt_buy_spread_neg_sell_no_export`; po deploy + replan BA81: žádný `grid < 0` při `sell < 0` v MCP. --- ## 2026-05-25 (l) — Plán 25. 5.: BA81 neg. výkup bez exportu, KV1 ranní curtail **Problém (MCP plán run 16346–16350, tag v7):** KV1 06–08 h masivní **curtail** FVE (plná baterie, `ge_pv=0` z pv_store). BA81 při `sell<0` **export ~10 kW** místo nabíjení. Večer slabý export u KV1/home-01 (spot: `sell < buy`). **Oprava (tag `2026-05-25-neg-sell-no-export-fixed-v8`):** - Fixní tarif (BA81): při **`sell < 0`** tvrdě **`ge = 0`** (jako KV1 s block_export) — přebytek jen baterie/curtail. - **`fixed_pv_b_export_cap`** jen při **`sell ≥ 0`** (po neg. okně export B). - KV1: **`skip_pv_store_block`** při kladném `sell` + PV přebytek — méně curtailu před neg. oknem. **Deploy:** služba v compose je **`backend`**, ne `ems-api`. Ověření: `docker compose -f /opt/ems-deploy/docker-compose.yml exec backend grep PLANNER_BUILD_TAG /app/services/planning_engine.py` --- ## 2026-05-24 (k) — BA81: Infeasible při SoC = 100 % (telemetrie = soc_max) **Problém:** Po v6 stále `Solver: Infeasible` při replanu, když `fn_planning_site_context` vrátí `soc_wh = soc_max_wh` (12 500). **Příčina:** Při dlouhém `sell < 0` a vysoké FVE MILP potřebuje alespoň ~**650 Wh** rezervy pod `soc_max` pro modelování PV→baterie / export B. Na přesně 100 % SoC je model neřešitelný (reprodukce na datech runu 16184). **Oprava:** tag **`2026-05-24-ba81-soc-headroom-v7`** — `_planner_soc_for_solver()` sníží vstupní SoC na `soc_max − max(650 Wh, 0,382×slot_nabíjení)`; v `solver_params.inputs.soc_headroom_applied_wh` je audit. **Ověření:** `pytest …::NegativeSellPvChargeTests`; replan BA81 s telemetrií 100 % → tag v7, bez Infeasible. --- ## 2026-05-24 (j) — BA81: Solver Infeasible (plná baterie + pole B + GEN cut-off) **Problém:** Po deployi večerních oprav u BA81 plánování padá na **`Solver: Infeasible`** (KV1 OK), typicky při **SoC ≈ 100 %** během dlouhého okna `sell < 0` (dnešní OTE). **Příčiny (dvě vrstvy):** 1. **v5:** `ge_pv = 0` z pv_store při `pv_b > 0` → oprava `ge_pv ≤ pv_b`. 2. **v6 (skutečný blocker u BA81):** `deye_gen_microinverter_cutoff_enabled` společně s `sell < 0` vynucovalo **`ge == 0`** (podmínka `z_gen_cutoff is not None`). Při plné baterii nelze nabít ani exportovat přebytek pole B → Infeasible. BA81 má v kontextu `soc_wh = soc_max_wh = 12 500`. **Oprava:** tag **`2026-05-24-ba81-gen-cutoff-v6`** — `ge == 0` jen při `block_export_on_negative_sell`; `ge_pv ≤ pv_b × (1 − z_gen_cutoff)`; v5 večerní push + pv_b cap zůstávají. **Ověření:** `pytest backend/tests/test_planning_dispatch_milp.py::NegativeSellPvChargeTests`; MCP po deployi: `planner_build_tag = 2026-05-24-ba81-gen-cutoff-v6`. --- ## 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. --- ## 2026-05-24 (f) — BA81: jen první slot sell<0 nabíjí 6 kW, další 1–2 kW **Problém:** Run **15838** — 06:15 Prague ~6,1 kW, 06:30–07:30 ~1,4–2,2 kW, 07:45–08:45 **0 kW** + curtail ~9 kW, 09:00+ znovu ~3 kW. Uživatel: „jen u prvního slotu se zápornou cenou“. **Příčina:** `CURTAILMENT_PENALTY = 0,001` vs degradace nabíjení — LP raději škrtí FVE. Oprava (e) pomohla jen prvnímu slotu (shortfall). Omezení `ca ≤ pv_a − bc_pv` bylo **špatně** (load-first: `pv_a_net` už závisí na `ca`). SoC v plánu stála ~51 % uprostřed okna, zbytek do curtailment. **Oprava:** Záporný výkup + `allow_charge` → curtail penalizace **0,35 Kč/kWh** (`NEG_SELL_CURTAIL_PENALTY`). Shortfall nabíjení **80 Kč/kWh**. Odstraněno `ca ≤ pv_a − bc_pv`. **Deploy:** jen **backend** restart + replan. --- ## 2026-05-24 (g) — BA81: plateau ~51 % SoC + curtail (run 15848/15849) **Problém:** Po replanu stále 06:15 ~6 kW, 06:30–07:30 ~1–2 kW, **07:45–08:45 0 kW + curtail ~9 kW**, SoC plán ~51 %, pak znovu ~3 kW. `solver_params` bez `planner_build_tag` → nasazený backend pravděpodobně **bez** oprav (e)/(f). **Příčiny (MCP + kód):** 1. **`charge_slots` v Pythonu** doplňoval `sell<0` jen při `block_export_on_negative_sell` (KV1). U BA81 (`false`) platily jen masky z DB → bez shortfall penalizace, i když R__063 nastaví `allow_charge` později. 2. **`safety_soc_target_wh`** z SQL roste jen k ~reserve + noční baseload (~50 % SoC v poledne). Jakmile `soc ≥ safety`, solver nemá motivaci dobít k `soc_max` v okně záporného výkupu (raději curtail / večerní export). 3. **`skip_pv_store_block`** u `pv_b` + fixní tarif: LP smí exportovat FVE při `sell<0` místo nabíjení (home-01 logika nepatří na BA81). **Oprava (`planning_engine.py`):** - `charge_slots` |= všechny sloty `sell<0` + PV přebytek > 500 W (jako R__063 ř. 787–791). - V okně `sell<0` + `charge_slots`: safety deficit cílí na `max(safety_sql, 92 % soc_max)`. - Fixní tarif: `ge_pv ≤ pv_b_forecast_w` při `sell<0`; `skip_pv_store` jen pro spot, ne fixed. - Objective: odměna `bc_pv` při `sell<0` (`NEG_SELL_PV_CHARGE_REWARD`). - `solver_params.planner_build_tag` = `2026-05-24-neg-sell-v2` (ověření deploye). **Deploy:** `docker compose build ems-api && docker compose up -d ems-api` + rolling replan BA81. **Ověření MCP:** ```sql select pr.id, pr.solver_params->>'planner_build_tag' as tag, max(pi.battery_setpoint_w) filter (where pi.effective_sell_price < 0) as max_neg_chg from ems.planning_run pr join ems.planning_interval pi on pi.run_id = pr.id where pr.site_id = (select id from ems.site where code = 'BA81') order by pr.id desc limit 1; ``` Očekáváno: `tag = 2026-05-24-neg-sell-v2`, v ranním okně `sell<0` více slotů s `battery_setpoint_w` ≥ 5000, SoC plán přes ~70 % směrem k 95 %. --- ## 2026-05-24 (i) — Večerní export BA81/KV1 + BA81 dobít na 100 % **Problém:** Po v3 KV1 nabíjení OK, BA81 stále plateau ~94 % v neg. okně. **Večer žádný prodej** z baterie ani při sell ~3,7 Kč (BA81 i KV1). **Příčiny:** 1. **`_slot_profitable_battery_export`:** u fixního tarifu porovnával `sell > acquisition + degrad` (BA81 acq ~3,61 → potřeba sell > ~3,91). Správně **`sell > buy + degrad`** jako v R__063. 2. **KV1 večer:** SQL večerní maska vyžadovala `sell > buy` (6,35 vs 3,7) → **`allow_discharge_export = false`**. 3. **LP:** `ge_bat >= export_push * z_export` — solver nechal **`z_export = 0`** (export „zdarma“ bez nutnosti). **Oprava:** `planning_engine.py` tag **`2026-05-24-evening-export-v4`**; `R__063` večerní peak u fixed tarifu bez podmínky sell>buy. Měkký push `ge_bat`, odměna `z_export`, `neg_sell_soc_underfill`, večerní export floor = min_soc. **Deploy:** `flyway migrate` (R__063) + rebuild `ems-api` + replan. MCP: `planner_build_tag = 2026-05-24-evening-export-v4`, večer `export_mode = BATTERY_SELL` nebo `grid_setpoint_w < -1000` v špičce. --- ## 2026-05-24 (h) — BA81: neg okno na plné soc_max (ne 92 %) **Problém:** Po (g) plán lépe nabíjí v okně `sell<0`, ale SoC plán končí ~**92 %** a drží se do přechodu na kladný výkup; až pak dobíjí na 100 %. **Příčina:** `NEG_SELL_CHARGE_SOC_FRAC_OF_MAX = 0.92` — umělý strop safety cíle v neg. okně. **Oprava (`planning_engine.py`, tag **`2026-05-24-neg-sell-v3`**): - Záporný výkup + PV: safety/shortfall cílí **`soc_max_wh`** (u BA81 100 %), ne 92 %. - Po posledním `sell<0` tentýž den: **`post_neg_pv_topup`** — dobití z FVE na `soc_max` před exportem při kladném sell (ne ve high-sell špičce). **Deploy:** rebuild `ems-api` + replan. MCP: `planner_build_tag = 2026-05-24-neg-sell-v3`, SoC v neg. okně ~100 % (resp. `planner_soc_max_wh`). --- ## Šablona pro další záznamy ```markdown ## YYYY-MM-DD — Krátký titul **Problém:** … **Změny:** … **Soubory:** … **Ověření:** … ```