Files
ems/docs/planning-changelog.md
Dusan Vojacek 5d06f49d2b
Some checks failed
CI and deploy / migration-check (push) Failing after 12s
CI and deploy / deploy (push) Has been skipped
oprava
2026-05-31 00:13:44 +02:00

940 lines
69 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
# 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-31 — home-01: evening push při každém relaxed retry (v55)
**Problém:** v54 maže tvrdý push až u `relaxed_neg_prep_window` (3. retry). Retry 12 pořád držely **vypočtený** `evening_push_ts` → u ~25 % SoC často stále **Infeasible**. Ruční „Přeplánovat“ navíc spadlo, když **v2 comparison** peer selhal (active v1 prošel). V DB po pádu **žádný `api` run** — scheduler v51/v54 mezitím OK.
**Změna (v55):** tvrdý `evening_push_ts = ∅` při **jakékoli** relaxed vlajce; rolling commitment ignorovat od `relaxed_neg_buy_charge`; comparison peer = `solve_dispatch_two_pass` + **non-fatal** skip. Snap: `evening_push_cleared_on_relaxed_prep`, `charge_commitment_ignored_on_relaxed`.
Tag **`2026-05-31-evening-push-any-relaxed-v55`**.
---
## 2026-05-31 — home-01: tvrdý evening push po relaxed prep (v54)
**Problém:** v53 maže jen **hysterézní override**, ne **vypočtený** `evening_push_ts`. Po `relaxed_neg_prep_window` (typicky home-01 ~25 % SoC + neg den 31.5.) zůstávaly tvrdé `ge_bat`/`z_export` v push slotech → **`Solver: Infeasible`** i po celém retry řetězci. Pass2 two-pass znovu aplikoval override bez carryover relaxace.
**Změna (v54):** při `relaxed_neg_prep_window``evening_push_ts = ∅`; `_solve_dispatch_relax_carryover` — pass2 dědí nouzové vlajky z pass1, `evening_push_ts_override=None`. Snap: `evening_push_cleared_on_relaxed_prep`.
Tag **`2026-05-31-evening-push-relaxed-clear-v54`**.
**Ověření MCP (home-01):** `planner_build_tag` = v54; po ručním replanu `relaxed_neg_prep_window: true`, `evening_push_ts: []`, run `status = active`.
---
## 2026-05-31 — home-01: Infeasible při rolling hysteréze push (v53)
**Problém:** Po v52 KV1 OK, **home-01** občas **`Solver: Infeasible`** — rolling replan držel `evening_push_ts` z minulého běhu (hystereze) i v retry větvích; tvrdý `ge_bat` push při nízkém SoC / změně slotů.
**Změna (v53):** `_evening_push_override_for_solve` — override **vypnout** při jakémkoli relaxed retry; `_filter_evening_push_override_indices` — jen sloty s `allow_discharge_export`, bez defer PV, s dosažitelným push floorem. Snap: `evening_push_override_dropped_on_retry`.
Tag **`2026-05-31-evening-push-override-retry-v53`**.
**Ověření:** `pytest … -k stale_evening_push_override`; rolling home-01 bez RuntimeError.
---
## 2026-05-31 — KV1: večerní push vs ranní max sell (v52)
**Problém:** KV1 večer **~3,3 Kč** neprodával do sítě (`evening_push` prázdný: `sell < acq+spread` ≈ 6,65), vývoz až **úsvit ~2,8 Kč** před `sell<0` (08:15). Příčina: pravidla **v41 `evening_early`** + **v47 push profitabilita** z home-01 na fixní acquisition.
**Změna (v52):** `_kv1_block_export_fixed_evening_push` — u **fixed + `block_export_on_negative_sell`** večerní push kandidát když `sell ≥ max(sell 511 před 1. sell<0) degrad` (ne `sell > 6,35+spread`). Bez neg dne v horizontu: `sell ≥ 1 Kč`. Snap: `kv1_evening_push_morning_peak_rule`.
Tag **`2026-05-31-kv1-evening-push-morning-peak-v52`**.
**Ověření:** `pytest … -k kv1_evening_push_when_sell_above_morning`; MCP KV1 večer `BATTERY_SELL`, `evening_push_ts` neprázdný.
---
## 2026-05-31 — BA81 úsvit: žádný plný curtail A / zápis reg 340 (v51)
**Problém:** Při malém ranním PV (např. **405 W** A, **49 W** B) LP kvůli `fixed_pv_b_export_cap` (`ge_pv ≤ pv_b`) **usekl celé pole A** (`curt_a = pv_a`) a exporter posílal **reg 340** z nepřesného forecastu — zbytečný HW zápis, baterie prázdná.
**Změna (v51):**
- `fixed_pv_b_export_cap` jen když **`pv_a_forecast ≥ 1500 W`** (`DAWN_LOW_PV_NO_CURTAIL_W`).
- **`fixed_mi_low_pv_surplus_export`:** úsvit + MI + přebytek → neblokovat `ge_pv` přes pv_store.
- **`setpoints.py`:** při `forecast < 1500 W` a `curt_a = 0`**`pv_a_allowed_w = None`** (bez reg 340).
Tag **`2026-05-31-ba81-dawn-no-micro-curtail-v51`**.
**Ověření:** `pytest … -k ba81_dawn_low_pv`; MCP BA81 05:15: `curt_a ≪ pv_a`.
---
## 2026-05-31 — KV1/BA81: při PV přebytku FVE→síť, ne bat→síť (v50)
**Problém:** **KV1** (`block_export`, fixní buy) odpoledne s FVE (~4 kW, sell&gt;0) plán **BATTERY_SELL** místo **PV_SURPLUS** (home-01 OK). Příčiny: `skip_pv_store_block` jen před 1. `sell&lt;0`; večerní **push** bez `defer_to_pv`; **z_export/ge_bat** u profitable peak.
**Změna (v50):**
- **`fixed_block_pv_surplus_export`:** KV1 + `sell≥0` + PV přebytek → neblokovat `ge_pv` (pv_store).
- **`battery_export_defer_pv_ts`:** `ge_bat=0`, `z_export=0` (výjimky: morning pre-neg / pre-neg buy větve).
- **`evening_push_ts`:** přeskočit push, když platí defer.
Tag **`2026-05-31-kv1-pv-surplus-over-bat-export-v50`**.
**Ověření:** `pytest … -k kv1_evening_battery_push`; MCP KV1 17:0018:00: `export_mode=PV_SURPLUS`, `curt_a` malé.
---
## 2026-05-31 — Večerní push: celý Wh rozpočet jen pro dnešní noc (v49)
**Problém (v43):** `push_budget / počet_kalendářních_večerů` dělil **aktuální SoC** mezi dnešní a **zítřejší** večer v horizontu — přes den FVE / neg nabíjení. Dnes večer dostal ~polovinu rozpočtu → chyběly sloty (např. 23:15); zítra večer push z dnešní SoC nedává smysl.
**Změna (v49):**
- **`_primary_night_export_segment_indices`** — první noční epizoda (17h → východ FVE) od začátku horizontu.
- **`_evening_push_soc_budget_calendar_segments`** — push Wh jen pro kalendářní večer v této epizodě; **jeden společný** rozpočet, kandidáti **sell desc** přes zbývající sloty.
- **Hysteréze** (`_rolling_evening_push_override`): drží jen sloty z budget-eligible množiny.
Tag **`2026-05-31-evening-push-budget-primary-night-v49`**. Zítřejší večer → vlastní rolling replan po dni.
**Ověření:** `pytest … -k evening_push_budget_only_primary`; MCP: `planner_build_tag` v49, `evening_push_ts` bez zítřejších 18:30+ při replanu dnes večer; více dnešních push slotů při stejné SoC.
---
## 2026-05-31 — Podlaha vývoje reserve 20 %, žádný curtail slabé FVE za úsvitu (v48)
**Problém (běh 20728, v47):** Večer + **03:0003:15** ranní peak export → SoC **~13,5 %** (pod **reserve 20 %**). **05:1506:00 Prague** (= 03:1503:45 UTC) plán **řeže celou PV A** (`curt_a = pv_a` při ~86346 W) — `ge_pv=0` kvůli `sell < future_sell` (večerní peak v horizontu).
**Změna (v48):**
- Rozpočet push + podlaha SoC: **`reserve_soc_wh`**, ne `min_soc_wh` (10 %).
- Ranní peak export: **`soc[t] ≥ reserve`** v peak slotu.
- **`DAWN_LOW_PV_NO_CURTAIL_W`:** při `sell≥0` a `pv_a < 1500 W` neblokovat `ge_pv` (žádný úsvitní curtail).
Tag **`2026-05-31-reserve-floor-no-dawn-curtail-v48`**. Pravidlo agenta: `.cursor/rules/ems-planning-agent-discipline.mdc`.
---
## 2026-05-30 — Po večerním pushu noc z baterie, ne import za 5 Kč (v47)
**Záměr uživatele:** Večerní vývoz za **~3 Kč/kWh** (sell&lt;buy) je **správně** — vyprázdnění před neg dnem/FVE. Špatně je **po pushu držet SoC a kupovat dům za ~5 Kč**.
**Problém (v45v46):** Po pushu **SoC ~36 %**, pak **22:00+ grid import** pro baseload; `relaxed_expensive_import` obešel `bd≥load`.
**Změna (v47):**
- **Večerní push:** zůstává **sell > acq+spread** (v46 sell≥buy **zrušeno**).
- **`post_evening_push_night_ts`:** po posledním push slotu večera → tvrdé **bd krmí dům** i při `relaxed_expensive_import`.
- **`night_self_consume`** + v45 neg okno beze změny.
Tag **`2026-05-30-post-push-night-battery-v47`**. (v46 na serveru nepoužívat — blokoval večerní push.)
---
## 2026-05-29 — Neg okno: grid nabíjení + noc z baterie (v45)
**Problém (v44 běh 20282):** (1) Po večerním pushu **22:00+** import ze sítě ~3,3 kW při SoC **56 %**`night_self_consume` jen na podmnožině `evening_early_export_ban`, ne celá noc. (2) **07:4508:15** sell&lt;0 prep: **`allow_charge=false`** (jen `pv_surplus>0`) → SoC stojí, **penalty ~11k Kč/slot**, solver **`relaxed_neg_prep_window`**. (3) **11:45** panické grid+bat 17 kW.
**Změna (v45):**
- **`_night_self_consume_discourage`:** všechny noční sloty mimo `evening_push` (buy &gt; acq+spread).
- **R__063 `neg_window_grid_charge`:** od 1. sell&lt;0 na neg den `allow_charge`+`allow_grid_charge` pro sell&lt;0 a buy≥0 i bez FVE přebytku.
- **LP:** při `relaxed_neg_prep_window` **bez** `prep_soc_shortfall` penalizace (žádné fiktivní 11k Kč).
Tag **`2026-05-29-neg-window-charge-night-v45`**.
---
## 2026-05-29 — Neg den: headroom pro FVE, ne grid za 3 Kč před sell&lt;0 (v44)
**Problém (v43 na home-01 30. 5.):** Ráno **05:4507:30** grid+bat nabíjení za **~2,63,7 Kč/kWh** → SoC **~99 %** ještě před **07:45 sell&lt;0**. Pak **PV A plně utlumena**, **PV B** do site za záporný sell; levný **buy ~0,48 Kč** v 11h nevyužit. Příčiny: (1) **`evening_arbitrage_unlock`** povolil drahý grid před neg oknem; (2) AM maska brala nejlevnější buy **před polednem**, ne v neg okně; (3) **`soc_need`** zpětně počítal jen **PV B**, ne A+B → cíl prep ≈ **soc_max**.
**Změna (v44):**
- **`evening_arbitrage_unlock`** jen na dnech **bez sell&lt;0**, hodiny **1116** (normální odpolední→večerní arbitráž).
- **`neg_day_no_grid_before_neg_sell`:** na neg kalendářní den **`allow_grid_charge=false`** pro všechny sloty **před 1. sell&lt;0**.
- **`_neg_sell_pv_forecast_charge_wh`:** zpětná projekce soc_need z **FVE A+B** surplusu, ne jen B.
- **LP:** `bc_gi[t]=0` před 1. sell&lt;0 na neg den (pás pro případ masky).
**Soubory:** `planning_engine.py`, `R__063_fn_load_planning_slots_full.sql`, `test_planning_dispatch_milp.py`, `planning.md`. Tag **`2026-05-29-neg-day-pv-headroom-v44`**.
**Ověření:** `pytest … -k "NegDayPvHeadroom or prep_leaves_headroom"`; MCP: před 07:45 `allow_grid_charge=false`, `grid_charge_suppressed_reason=neg_day_no_grid_before_neg_sell`; SoC před neg &lt; ~90 %; po svítání PV A ne plný curtail.
---
## 2026-05-29 — Noc: vlastní spotřeba + večerní arbitráž + push per den (v43)
**Problém:** (1) Po v42 push exportu plán přes noc **držel SoC ~60 %** a krmil dům ze sítě za **~5 Kč/kWh** místo baterie (acq ~0,7 Kč). (2) Tvrdý push zahrnoval **0206h** (sell &lt; buy). (3) **Druhý večer** v horizontu neměl push — rozpočet Wh se vyčerpal první nocí. (4) Před neg dnem **grid 0,5 Kč** odpoledne nešel nabíjet (`allow_charge=false`, cheaper_pv_ahead), přitom večer sell **~4 Kč** — arbitráž neproběhla.
**Změna (v43):**
- **`night_self_consume_discourage_ts`:** mimo `evening_push` penalizace importu pro dům (`gi × surcharge`), LP preferuje `bd` pro load.
- **Push jen ≥17h Prague** (`_in_evening_push_hour_window`); ne predawn 0206h.
- **Push rozpočet per kalendářní večer** (`_evening_push_calendar_segments`), ne globální greedy přes celou noc.
- **Push kandidáti** jen `allow_discharge_export` (SQL maska).
- **R__063 `evening_arbitrage_unlock`:** před prvním sell&lt;0 povolit grid nabíjení, když tentýž den večer (≥17h) `buy + degrad < evening_peak_sell`.
**Soubory:** `backend/services/planning_engine.py`, `db/routines/R__063_fn_load_planning_slots_full.sql`, `backend/tests/test_planning_dispatch_milp.py`, `docs/04-modules/planning.md`. Tag **`2026-05-29-night-selfconsume-evening-arb-v43`**.
**Ověření:** `pytest … -k "evening or night_self or predawn or per_calendar"`; MCP: `night_self_consume_discourage_ts`, druhý den v `evening_push_ts`; odpoledne `allow_charge=true` + `grid_charge_suppressed_reason=evening_arbitrage_unlock`; mezi push sloty `battery_setpoint_w < 0`, `grid_setpoint_w ≈ 0`.
---
## 2026-05-29 — Večerní push: rozpočet Wh × sell desc (v42)
**Problém:** v41 bral push kandidáty jen jako sloty s **`sell = max`** v nočním úseku → při ~48 kWh rozpočtu často **jediný** push slot (~13,5 kW), zbytek energie „visel“ v baterii; levnější profitable sloty byly zákázané (`evening_early`), ale dražší sousední sloty pod maximem se nevyužily.
**Změna (v42):**
- Kandidáti = **všechny profitable** sloty v nočním okně (`acq+spread`, ne fixní buy).
- Push = **sell desc** greedy fill, dokud `kumulované_Wh ≤ push_budget` (globální rozpočet přes noční úseky).
- `evening_early` (`ge_bat=0` mimo push) a vypnutý `peak_export_shortfall` v noci **beze změny**.
**Soubory:** `backend/services/planning_engine.py` (`_evening_push_segment_candidates`, `_evening_battery_export_push_indices`), `backend/tests/test_planning_dispatch_milp.py` (`test_evening_no_spread_export_below_segment_peak_home01`, `test_evening_push_respects_wh_budget_not_all_profitable_slots`). Tag **`2026-05-29-evening-push-budget-rank-v42`**.
**Ověření:** `pytest … -k evening_no_spread`; MCP: `solver_params->'inputs'->'evening_push_ts'` — délka ≈ `floor(budget_wh / per_slot_wh)`; každý push slot → `|grid_setpoint_w|` ≈ 12,513,5 kW; sloty mimo push → bez exportu.
---
## 2026-05-29 — Večerní export jen ve špičkových slotech (v41)
**Problém:** home-01 večer ~7,5 kW export v mnoha levnějších slotech (~3,2 Kč) místo plného **13,5 kW** v max-sell slotu. Tři důvody: (1) `evening_push` kandidáti = široké pásmo **peakdegrad** (0,15 Kč); (2) měkká penalizace **`peak_export_shortfall`** tlačila `ge_bat` i v levnějších nočních slotech; (3) push se neaktivoval, když horizont měl **konstantní buy** → mylně „fixní tarif“ a `sell < buy` (přitom večerní export dává smysl vůči `acq+spread`).
**Změna (v41):**
- Push kandidáti = sloty se **`sell = max`** v nočním úseku + marže **`acq+spread`** (spot), ne `buy+spread`.
- **`evening_early_export_ban`:** `ge_bat=0` ve **všech** nočních exportních slotech mimo `evening_push` (výjimky: pre-neg / neg-evening větve).
- **`peak_export_shortfall`** se v nočním okně neaplikuje.
**Soubory:** `backend/services/planning_engine.py` (`_evening_push_peak_candidates`, `_evening_early_export_penalty_indices`), `backend/tests/test_planning_dispatch_milp.py` (`test_evening_no_spread_export_below_segment_peak_home01`). Tag **`2026-05-29-evening-peak-only-export-v41`**.
**Ověření:** `pytest … -k evening_no_spread_export_below_segment_peak_home01`; MCP: večerní slot s max sell → `|grid_setpoint_w|` ≈ 12,513,5 kW; sousední levnější sloty → `export_mode=NONE`, `grid_setpoint_w≥0`.
---
## 2026-05-29 — Infeasible rolling: relax neg-prep okno (v40b)
**Problém:** Po načtení OTE na **30. 5.** (neg sell) rolling/home-01 končil `Solver: Infeasible` od ~13:15; ruční replan stejně. Plán zůstal na runu z 13:00 (horizont jen do 22:00). Log často prázdný — výjimka se loguje na `WARNING`, scheduler ji polyká.
**Změna (v40b):** Třetí retry `relaxed_neg_prep_window` (bez večerního push/kotvy + prep hold binárek); čtvrtý retry s `planner_neg_sell_prep_soc_percent=100` (fáze sell&lt;0 vypnuté). Večerní push jen sloty s `allow_discharge_export`. Rolling v **MANUAL** se přeskočí (log INFO). Tag **`2026-05-29-neg-prep-infeasible-relax-v40b`**.
**Ověření:** po deployi `POST …/plan/run?type=rolling` v AUTO; `solver_params.inputs.relaxed_neg_prep_window` nebo `neg_sell_phases_fallback`; log: `docker compose -f deploy/docker-compose.yml logs backend --since 2h 2>&1 | rg -i infeasible`.
---
## 2026-05-29 — Neg-prep z pozorovaného SoC (Plan 5, v40)
**Problém:** Strategie „místo na zítřejší FVE + sell&lt;0“ a večerní výboj před neg dnem počítaly z **modelového** SoC (řetězení `soc_target` mezi dny v `_pre_neg_pv_export_bundle`). BMS měl často **~15 %** více → předčasné zastavení výboje, „mrtvé“ kWh přes noc, méně ranního pre-neg exportu.
**Změna (v40):**
- `observed_soc_wh` = telemetrie před `_planner_soc_for_solver`; cushion v33/v36 vždy z něj (bez `soc_est` řetězení).
- `_pre_neg_pv_export_forecast_cushion_ok_for_day`: pokud `observed_soc ≥ target` → cushion OK.
- Večerní push před neg: `neg_evening_export_budget_wh = max(0, observed reserve night_baseload_buffer)`; tvrdý shortfall jen v `neg_evening_push_slots` (nejdražší sloty v rozpočtu).
**Soubory:** `backend/services/planning_engine.py`, `backend/tests/test_planning_dispatch_milp.py` (`ObservedSocNegPrepTests`), `docs/04-modules/planning-neg-sell-strategy.md`, `docs/04-modules/planning.md`.
**Ověření:** `pytest … -k ObservedSocNegPrep`; MCP: `solver_params->'inputs'->>'observed_soc_wh'`, `neg_evening_export_budget_wh`, `neg_evening_push_slots`. Tag **`2026-05-29-neg-prep-observed-soc-v40`**.
---
## 2026-05-29 — Exekuční pojistka exportu (Plan 3)
**Problém:** Plán `export_mode = NONE` nebo záporná vykupní, ale Deye zůstává v **SELL** → skutečný vývoz ~12 kW (zpoždění přepnutí režimu).
**Změna:** `_apply_export_plan_guard` v `setpoints.py` (volá `orchestrator.export_setpoints` před `_apply_price_failsafe_guard`): při `sell < 0` nebo (`export_mode = NONE` a `grid_setpoint_w ≥ 0`) vynutí PASSIVE, `export_ban`, `grid_export_limit = 0`, vynulování vybíjení v plánu (`battery_w ≥ 0`). SQL guard **`NEG_SELL_EXPORT`** v `R__076_fn_plan_actual_slot_guard.sql` (`sell < 0` a vývoz &lt; 4 kW).
**Soubory:** `backend/services/control/setpoints.py`, `orchestrator.py`, `db/routines/R__076_fn_plan_actual_slot_guard.sql`, `backend/tests/test_control_export_plan_guard.py`, `docs/04-modules/control.md`, `docs/04-modules/modbus-command-journal.md`.
**Ověření:** `pytest backend/tests/test_control_export_plan_guard.py`; po incidentu Discord s `reason_code = NEG_SELL_EXPORT`.
## 2026-05-28 — Dokumentace strategie sell&lt;0 + termika + bazén
**Soubor:** [`docs/04-modules/planning-neg-sell-strategy.md`](04-modules/planning-neg-sell-strategy.md) — cíle, slovník, časová osa dne, v32v35, návrh v36+, TČ/TUV podle typu dne, bazén, UI curtail/reg 340, roadmap, SQL ověření.
**Rozhodnutí home-01** (souhrn v [`docs/06-open-questions.md`](06-open-questions.md)): rampa/**T** odvozené z PV B (bez fixních 80 % v LP); TČ ne v pre-neg exportu; bazén min 4 h/den + Shelly; spirála Loxone; **workshop UI flex zátěží před v37** (§ 9.1 strategie).
## 2026-05-28 — Večerní export: dynamický Wh push + hysteresis (v38)
**Problém:** `_evening_battery_export_push_indices` bral jen **málo slotů** v úzkém pásmu `max0,05` a při řazení podle rozpočtu mohl vynechat dražší 15min (9,5 Kč) a exportovat později levněji (4,8 Kč). `evening_early` zákaz `ge_bat` platil jen **před** prvním push slotem.
**Změna (v38):** Kandidáti = **profitable ∩ peak pásmo v nočním okně** (`_evening_peak_export_indices`, max sell v úseku degrad — shodně s R__063); push = nejdražší **sell desc**, dokud `kumulované_Wh ≤ push_budget` (`discharge_slot_buffer`, SoC nad `min_soc`); `per_slot` = min(BMS, export cap) × účinnost × 0,25 h — **počet slotů dynamický** (např. ~40 kWh / ~3,4 kWh ≈ 11 slotů u home-01), **ne pevné top-3**. `evening_early` = `ge_bat=0` pro profitable noční sloty pod `peak0,05` mimo `evening_push_ts` (i po prvním push). Rolling **hysteresis** při malé změně peak sell / SoC. (Doplněno ve v39: stejná logika, tag `evening-export-soc-balance-v39`.)
**Soubory:** `backend/services/planning_engine.py`, `backend/tests/test_planning_dispatch_milp.py`, `docs/04-modules/planning.md`.
**Ověření:** `pytest … -k evening`; tag **`2026-05-28-evening-export-dynamic-v38`**. `solver_params.inputs.evening_push_ts` — délka ≈ `floor(push_budget_wh / per_slot_discharge_wh)`.
## 2026-05-28 — SoC bilance: jen `bd`, ne `bd+ge_bat` (v39)
**Problém:** SoC kontinuita odečítala **`bd + ge_bat`**, ale z energetické bilance `pv + gi + bd = load + bc + ge` už platí **`bd ≈ load + ge_bat`** při exportu z baterie → pokles SoC **~2×** rychleji než BMS ve večerním `BATTERY_SELL`. v37 kalibrace (`discharge_calibration_factor`) to jen maskovala.
**Změna (v39):** SoC rovnice: ` bd[t] / discharge_efficiency × interval_h` (bez druhého `ge_bat`). Odstraněno: `fn_soc_tracking_bundle`, `_soc_tracking_bundle`, `discharge_calibration_factor`.
**Soubory:** `backend/services/planning_engine.py`, `db/routines/R__091_fn_soc_tracking_bundle.sql` (drop), `backend/tests/test_planning_dispatch_milp.py` (`SocBalanceDischargeTests`), `docs/04-modules/planning.md`.
**Ověření:** `SocBalanceDischargeTests::test_export_slot_soc_drop_not_double_ge_bat`; MCP po deploy: `planner_build_tag = 2026-05-28-evening-export-soc-balance-v39`, drift `plan_soc vs actual_soc` při večerním výboji.
## 2026-05-28 — SoC tracking + discharge_calibration_factor (v37, nahrazeno v39)
**Problém:** LP bilance SoC při výboji klesala o **1525 %** rychleji než BMS → méně `BATTERY_SELL` ve večerní špičce, energie zbytečně „na zítra“.
**Změna (v37):** `ems.fn_soc_tracking_bundle` + `_soc_tracking_bundle` v rolling replanu; `discharge_calibration_factor` násobí `(bd + ge_bat)` **jen v rovnici kontinuity SoC** (`solve_dispatch`). Konstanty: error práh 3200 Wh, min výboj 1000 Wh, factor clamp 0.51.2.
**Soubory:** `backend/services/planning_engine.py`, `db/routines/R__091_fn_soc_tracking_bundle.sql`, `docs/04-modules/planning.md`.
**Ověření:** `SocTrackingDischargeCalibrationTests`; MCP po večerním výboji: `solver_params->'inputs'->>'discharge_calibration_factor'`, `|plan_soc actual_soc| < 8 %` po ~2 h (cíl &lt; 5 % po doladění). Tag **v37**. **→ Root cause opraven v39; kalibrace zrušena.**
**Problém (v36f):** BA81 — `skip_pv_store` nestačil: `fixed_pv_b_export_cap` držel `ge_pv ≤ pv_b` → curtail pole A. home-01 rolling — prázdné `neg_evening_*` (D1 večer mimo horizont), SoC ~29 % místo ~20 % před `sell<0`.
**Změna (v36g):** Fixed pre-neg: `ge_pv ≤ pv_surplus` (A+B). Spot neg: kotva i na `first_neg1` + výboj ve **všech** kladných sell slotech před 1. `sell<0` (ne jen D1 večer).
**Ověření:** `test_ba81_fixed_morning_exports_pv_a_not_curtail`, `test_rolling_horizon_drains_to_reserve_before_first_neg`; tag **v36g**.
**Deploy verified (2026-05-28, MCP `user-postgres-ems`):** Všechny aktivní rolling runy (`home-01`, `BA81`, `KV1`, `hulin-bess`) mají `planner_build_tag = 2026-05-28-neg-prep-window-v36g`. BA81 run 19604: před 1. `sell<0` (29.5. 10:15 Prague) u 19 slotů s PV přebytkem `pv_a_curtailed_w = 0`, `|grid_setpoint_w| = pv_surplus` (0 curtail/export mismatch). home-01 run 19560: `neg_evening_reserve_soc_anchors` délka 2 (kotvy 28.5. 23:45 a 29.5. 10:00 Prague, `target_reserve_soc_wh` 12800), večerní výboj k ~20% SoC před neg oknem.
## 2026-05-28 — Fixed tarif: export FVE před sell&lt;0 (v36f)
**Problém:** BA81 (fixed, sell&gt;3 Kč ráno): plán **curtail** PV A (~3 kW) + export jen **~600 W** (`ge_pv` jen přes pole B). Střídač reálně valí celou FVE — ekonomicky správně, ale plán nesedí. Příčina: `ge_pv=0` při `sell &lt; future_sell` (pv_store); `fixed_pv_b_export_cap` uvolní jen MI.
**Změna (v36f):** `skip_pv_store_block` i pro **všechny fixed** sloty před prvním `sell&lt;0` při `sell≥0`. `export_mode`: **BATTERY_SELL** jen když `ge_bat` je významný (≥500 W), jinak **PV_SURPLUS** (oprava matoucího labelu při ~600 W exportu).
## 2026-05-28 — KV1 fixed + block_export (v36e)
**Kód:** `planning_engine.py` tag `2026-05-28-neg-prep-window-v36e`; `R__063_fn_load_planning_slots_full.sql`.
**Problém:** KV1 (fixní buy ~6,35, jen PV A, `block_export_on_negative_sell`) — od v34/v36 logiky pro spot/home-01: ráno **curtail** místo exportu do site; večer jen **jeden** discharge slot (sell peak 6,57 vs buy 6,35). BA81 má pole B (`fixed_pv_b_export_cap`) a nižší buy → chová se správně.
**Změna:** `skip_pv_store_block` pro fixed+block_export bez PV B při `sell≥0`; večerní `evening_peak_export_ts` + profitable export pro všechny kladné sell sloty v nočním okně; SQL maska `allow_discharge_export` stejně pro KV1 večer.
**Ověření:** `PreNegativeSellExportTests` (s `purchase_pricing_mode=fixed`); po deployi KV1 plán: odpoledne `PV_SURPLUS` / export, večer více `BATTERY_SELL` slotů.
## 2026-05-28 — Přípravné okno neg dne (v36 / v36b / v36d)
**Kód:** `backend/services/planning_engine.py` — tag `2026-05-28-neg-prep-window-v36d`.
**Změna (v36):** Bod **T**, pre-neg per den (cushion A+B), večerní `neg_evening_before_neg_slots`.
**Změna (v36b):** Kotva **`neg_evening_reserve_soc_anchors`** — SoC na konci večera D1 ≤ **`reserve_soc_wh`** (+ slack). **Chyba:** slack horní mez = `soc_max reserve` → LP nechal ~50 % SoC (penalizace 4 Kč/Wh na obří slack).
**Změna (v36d):** Slack max **400 Wh**, penalizace **55 Kč/Wh**; večerní `ge_bat` shortfall **bez** filtru profitable export; exportní podlaha u `neg_evening_before_neg_ts` = **`min_soc`** (ne `arb_base`). Kotva jen **večer D1** (ranní slot před 1. `sell<0` nekoliduje s prep rampou).
**Ověření:** `NegSellPrepWindowV36Tests` (vč. `test_evening_reserve_soc_near_reserve_after_discharge`); MCP: `planner_build_tag` = v36d, `battery_soc_target_pct` u kotvy ≤ ~22 % (reserve 20 % + slack).
## 2026-05-28 — Rampa SoC z PV B, bod T (v35)
**Kód:** `backend/services/planning_engine.py` — tag `2026-05-28-neg-sell-b-ramp-v35`.
**Změna:** `_neg_sell_day_phases` počítá `soc_need[t]` zpětnou projekcí jen z PV B; prep cíle = rampa (ne fixních 80 %). **t_detach**, **E_surplus_after_t** v `solver_params.inputs`. Prep hold na `soc_target[t]` z rampy; po T měkké `NEG_SELL_POST_DETACH_BCPV_DISCOURAGE`. Cushion v33: cíl z rampy, usable jen z B.
**Ověření:** `pytest tests/test_planning_dispatch_milp.py -k "NegSell or PreNeg or LoadFirst"`; MCP `solver_params.inputs.neg_sell_day_meta`.
## 2026-05-28 — Tvrdý load-first v LP (v34)
**Problém:** V sell&lt;0 prep plán ukazoval `grid_setpoint_w ≈ load_baseline` při FVE ≫ load — LP účetně posílal dům přes `gi`, zatímco Deye load-first krmit dům z FVE.
**Změna (tag `2026-05-28-load-first-hard-v34`):** `gi ≤ bc_gi + max(0, max_load pv_forecast)`; při dostatečné FVE `pv_ld ≥ load` (žádný fiktivní import = load při vysoké FVE). Test `LoadFirstDispatchTests::test_neg_sell_prep_no_fictitious_grid_import_for_load`.
## 2026-05-28 — Před sell&lt;0: export FVE jen při dostatečné predikci v záporném okně (v33)
**Problém:** Při kladném sell ráno LP nabíjel na večerní peak (~6,5 Kč) místo exportu (~3 Kč). Uživatel chce export teď, ale ne když forecast v sell&lt;0 okně nestačí na dobítí (déšť).
**Změna (tag `2026-05-28-pre-neg-pv-export-forecast-v33`):** `_pre_neg_pv_export_forecast_cushion_ok` — porovná potřebné Wh na prep SoC (80 %) s odhadem FVE v sell&lt;0 slotech téhož dne (`_neg_sell_day_pv_usable_wh` × margin 1,15). Jen pak `pre_neg_pv_export_ts` + shortfall `ge_pv` + **`bc_pv=0`** (ranní FVE ne do baterie). Jinak staré chování (šetřit na večer / nabít z FVE).
**Ověření:** `pytest … -k PreNegPvExportForecastTests` · `solver_params.inputs.pre_neg_pv_export_forecast_ok`.
## 2026-05-28 — Záporný výkup: fázované SoC a curtail A (v32)
**Problém:** V okně `sell < 0` LP tlačil `soc_max` až na konci; nepraktické pro EV/TČ/oblačnost; curtail A na FE málo viditelný.
**Změna (tag `2026-05-28-neg-sell-soc-phases-v32`):** Sloupce na `ems.asset_battery`: `planner_neg_sell_prep_soc_percent` (default 80), `planner_neg_sell_full_soc_tail_slots` (default 4), `planner_neg_sell_vent_min_sell_czk_kwh` (default 1 u home-01). `_neg_sell_day_phases` v `solve_dispatch`: **prep** (ASAP na prep %), **tail** (rampa na `soc_max`, ventil B pokud `sell ≥` práh), měkké curtail A přes `pv_a_curtailed_w` → reg 340. Legacy: `prep_soc_percent ≥ 100` nebo `tail_slots = 0`. KV1 s `block_export_on_negative_sell`: seed `prep=100`.
**Ověření:** `pytest … -k NegSellSocPhaseTests` · `planner_build_tag` **v32** · FE sloupec PV A + badge sell prep/tail.
## 2026-05-28 — Ráno: FVE do sítě místo plného ge_bat push (v31)
**Problém (run 17622, 07:00):** Při `sell ≥ 0` a PV přebytku `pre_neg_buy_discharge` vynutilo `ge_bat ≈ 13,5 kW` → exportní cap obsadila baterie → **celý curtail PV A** (v29 `ge_pv` sice povoleno, ale bez kapacity).
**Změna (tag `2026-05-28-morning-pv-export-priority-v31`):** `_battery_export_push_defer_to_pv` — u kladného sell + `pv > load + 500 W` se **neaplikuje** tvrdý/měkký push `ge_bat` z `pre_neg_buy_discharge`, `pre_neg_buy_empty`, `morning_pre_neg_export`, `peak_export_shortfall`. Večerní `evening_push` beze změny.
**Ověření:** `pytest … -k morning_pre_neg_discharge_exports_pv` · `planner_build_tag` **v31**.
## 2026-05-28 — Noční export přes půlnoc, konec při východu FVE (v30)
**Problém (home-01 run 17388):** Večerní peak **per kalendářní den** → export v **23:30** (3,29 Kč), slot **00:00** (3,59 Kč) bez `BATTERY_SELL` (nový den, hour &lt; 17).
**Změna (tag `2026-05-28-night-export-window-midnight-v30`):** `_night_export_window_segments` — okno **≥17h** + **05h** Prague, konec při `pv_a+pv_b > load + 500 W`. `_evening_peak_export_indices` / push / `evening_early` používají **jeden max sell v nočním úseku** (přes půlnoc). Po východu FVE žádný tvrdý push baterie.
**Ověření:** `pytest … -k night_window_includes_midnight or midnight_higher_sell` · `planner_build_tag` **v30**.
## 2026-05-28 — FVE při kladném sell: solver místo pv_store curtail (v29)
**Problém (home-01 odpoledne):** `ge_pv = 0` když `sell < max(future_sell)` (např. 3 Kč vs. večerních 6 Kč) při plné baterii → **curtail** celého pole A. Záměr „držet na večerní peak“ měl platit pro **baterii** (`ge_bat`), ne blokovat export FVE.
**Změna (tag `2026-05-28-pv-positive-sell-solver-v29`):** `skip_pv_store_block` u spotu pro **`sell ≥ 0`** + PV přebytek (home-01 i KV1). Tvrdý `ge_pv = 0` zůstává pro **`sell < 0`** (a fixní tarif dle `fixed_pv_b_export_cap`). Večerní export baterie beze změny (v28).
**Ověření:** `pytest … -k Home01PvStoreValueTests` · `planner_build_tag` **v29** · odpolední slot: export FVE (`grid_setpoint_w < 0`), ne plný curtail.
## 2026-05-28 — večerní export: plný site cap (v28)
**Problém (v27):** Push používal `ge_bat ≤ (max_dischargeload)/2` kvůli LP limitu `bd+ge_bat ≤ BMS` při bilanci `bd≈load+ge_bat` — plán ~8 kW místo až **13,5 kW** (home-01).
**Změna (tag `2026-05-28-evening-peak-full-export-v28`):** Push cap `min(export_cap, max_dischargeload)`; v `evening_push_ts` BMS **`load + ge_bat ≤ max_discharge`** místo `bd+ge_bat`. Deye realtime dál řídí load-first na zařízení.
**Ověření:** `pytest … -k evening_push_export_near_site_cap_home01` · `planner_build_tag` **v28** · `|grid_setpoint_w|`**13,5 kW** při typickém večerním load ~1,8 kW.
## 2026-05-28 — večerní export: oprava home-01 bez prodeje (v27)
**Problém (v26, home-01 run 17010):** Večer baterie vybíjela jen do domu (`export_mode` NONE, `grid_setpoint_w` 0). Dva důvody: (1) `evening_early` (`ge_bat=0`) platilo i **po** nejvyšším sell slotu, takže 1921 h nemohly exportovat; (2) při **drahém importu** (`buy` ≫ ranní `ref_buy`) bilance s `gi≈0` dává `ge_bat≈0` při `bd≈load`, takže tvrdý push na `ge_bat` bez `bd≥load+ge_bat` byl neřešitelný / ignorovaný; **terminal SoC** dále tlumil `z_export`.
**Změna (tag `2026-05-28-evening-peak-full-export-v27`):** `evening_early` jen pro sloty **před** `min(evening_push_ts)`; push: `ge_bat` cap ≈ `(max_dischargeload)/2`, `bd+ge_bat≥load+ge_bat`, `z_export=1`; vyšší bonus `EVENING_PUSH_Z_EXPORT_BONUS_CZK`. Detail: [`planning.md`](04-modules/planning.md).
**Ověření:** `pytest … -k evening_peak_battery_export` · po deployi `planner_build_tag` **v27** · večerní špička: `BATTERY_SELL` a `|grid_setpoint_w|` řádově kW (ne jen vybíjení do load).
## 2026-05-28 — večerní export: plný výkon u top sell, bez předčasného vybití (v26)
**Problém:** Ve **stejném večeru** LP rozlévalo vývoz baterie do více slotů v širokém pásmu „denní večerní max degrad“ (řádově 0,15 Kč/kWh), často jen na **~50 %** výkonu (např. ~3,1 kW místo 6,25 kW u BA81). Před **nejdražší** čtvrthodinou už nezůstala energie na plný výkon; Deye pak jede na hard cap, ale plán to neodrážel (`grid_setpoint_w ≈ 1` při `BATTERY_SELL` u home-01).
**Změna (tag `2026-05-28-evening-peak-full-export-v26`)** — doplňuje v24 (Wh rozpočet), **nemění** globální ekonomiku LP. Detail: [`docs/04-modules/planning.md`](04-modules/planning.md) sekce *Večerní export z baterie*.
| Mechanismus | Co dělá | Co **nedělá** |
|-------------|---------|----------------|
| Globální LP | Max. zisk v horizontu; export kde sedí marže a masky | Není „jen jeden večerní slot“ |
| `evening_early` (`ge_bat = 0`) | Od **17:00**: `sell < denní_večerní_max 0,05` Kč/kWh — baterie nevybíjí *před* absolutní špičkou | **Neplatí ráno**; neblokuje `ge_pv` |
| `evening_push` | Top večerní sloty (≥ max0,05): **plný** `ge_bat`; **počet slotů** = Wh rozpočet, řazení `sell` desc | Není jediný slot; není široké peakdegrad pro push |
| `_dispatch_grid_setpoint_w` | `grid_setpoint_w` z `ge` / `ge_bat` pro Deye reg 143 | — |
**Ověření:** `pytest … -k evening_peak_battery_export_at_site_cap` · `planner_build_tag` = **v26**.
---
## 2026-05-28 — reg 340 cap z výkonu střídače, min dle firmware (V082)
**Změna:** `asset_inverter.deye_reg340_max_solar_w` / `deye_reg340_min_solar_w`; `fn_inverter_pv_a_max_w` bere strop z DB sloupce (home-01 **32 000 W**, ostatní Deye **65 000 W**), ne součet Wp polí — studené panely mohou překročit nominál. `compute_pv_a_reg340_max_solar_w(..., min_w=)` — spodní limit jen pro kladné hodnoty (home-01 min **400 W**).
**Ověření:** `select ems.fn_inverter_pv_a_max_w(<deye-main id>);` · `pytest backend/tests/test_control_exporter_reg340.py`.
---
## 2026-05-28 — reg 340 jen když plán curtailuje / exportuje / nabíjí
**Změna:** `plan_skips_deye_reg340_write` v `setpoints.py` — bez FC 0x10 na reg **340**, pokud slot nemá export, nabíjení baterie ani `pv_a_curtailed_w` (Deye řídí PV A přes 108/109/142).
**Ověření:** `pytest backend/tests/test_control_exporter_reg340.py`.
---
## 2026-05-28 — dvoufázová SoC před buy&lt;0, PV A curtail jen v buy&lt;0 (v25)
**Požadavek:** (1) **PV A omezení** jen při `buy&lt;0` — raději import se ziskem než „zdarma“ ze střechy. (2) **Před `buy&lt;0`** dostatečně **nízké SoC** (vejde import v okně + PV B + rezerva na odpolední `sell&lt;0`). (3) **Nejpozději při posledním `sell≥0` před `buy&lt;0`** baterie **~100 %** (bez exportu — PV do bat). (4) Ranní `sell&lt;0` před `buy&lt;0`: PV smí do baterie (ne tvrdé `bc_pv=0`).
**Oprava (tag `2026-05-28-pre-neg-buy-soc-phases-v25`):** `_pre_neg_buy_soc_ceiling_wh`, kotvy `soc` na `last_pos_sell` (max) a `first_neg_buy-1` (strop), `pre_neg_buy_empty_ts` výboj, `pos_sell_pre_neg_buy_ts` `ge=0`, `bc_pv=0` jen při `buy&lt;0`, `NEG_SELL_CURTAIL` jen `buy&lt;0`, ranní PV charge shortfall.
**Ověření:** `pytest backend/tests/test_planning_dispatch_milp.py -k PreNegBuySocPhase`.
---
## 2026-05-28 — dynamický večerní push (v24)
**Problém:** Tvrdý večerní push používal pevné **`max_slots_per_day = 3`** a aktivaci jen při **`len(evening_push_ts) ≥ 2`** — nesouvisí s `discharge_slot_buffer`, SoC ani počtem večerních peak slotů (changelog v17 mluvil o top-6/≥7, v kódu bylo 3/2).
**Oprava (tag `2026-05-28-evening-push-dynamic-budget-v24`):** `_evening_push_discharge_budget_wh` + `_evening_battery_export_push_indices` — kandidáti = večerní peak ∩ maržní export; řazení `sell desc`; přidávat sloty dokud `kumulované_Wh ≤ min(available_soc, exportable_full × discharge_slot_buffer)` (`per_slot` = max_discharge × účinnost × 0,25 h). Jedna i více slotů podle rozpočtu; žádný pevný top-3.
**Ověření:** `pytest backend/tests/test_planning_dispatch_milp.py -k EveningPushBudget` a celý soubor MILP.
---
## 2026-05-28 — noční/ranní výboj baterie před buy&lt;0 (v23)
**Požadavek:** Před ranním oknem záporných cen **vybít baterii do sítě** (ne jen ~500 W do domu), aby zůstala kapacita na levný import v `buy&lt;0`.
**Oprava (tag `2026-05-28-pre-neg-batt-discharge-v23`):** `_pre_neg_buy_discharge_indices` — sloty `t &lt; first_neg_buy_idx`, `sell ≥ 1` Kč/kWh, marže exportu z baterie; **`ge_bat`** + shortfall + push na DB export cap, **bez** přidání do `discharge_export_slots` (v19b). Výjimka z `ge_bat=0` v pre-selection; exportní SoC podlaha `min_soc`.
---
## 2026-05-28 — rozlišení buy&lt;0 vs sell&lt;0 (v22 / v22b)
**v22b — Infeasible:** Tvrdý `is_daytime_pv_surplus` + `ge_pv=0` z pv_store blokoval export před buy&lt;0. Oprava: jen měkká `PRE_NEG_CHARGE_PENALTY`; u `buy&lt;0` přeskočit sell&lt;0 ventil; `neg_buy` shortfall jen na **posledním** buy&lt;0 slotu; retry `relaxed_neg_buy_charge`. Tag `2026-05-28-buy-sell-split-v22b`.
---
## 2026-05-28 — rozlišení buy&lt;0 vs sell&lt;0 (v22, superseded by v22b)
**Problém (MCP run 16706, v21b):** Znaménka v objective OK (`grid&lt;0` = export, `bat&gt;0` = nabíjení). Chování ale „opačně“:
- **Před buy&lt;0** (05:3007:00, buy≥0): nabíjení z PV/sítě místo přípravy kapacity.
- **Při buy&lt;0** (12:1512:45): export do sítě místo importu — ventil `w_pv_b_vent` u sell&lt;0 platil i když buy&lt;0.
**Oprava (tag `2026-05-28-buy-sell-split-v22`):**
- **Před `first_neg_buy_idx` a buy≥0:** tvrdé `bc_pv=bc_gi=0` jen v `is_daytime_pv_surplus_slot` (SQL); jinak měkká penalizace `PRE_NEG_CHARGE_PENALTY`.
- **sell&lt;0 a buy≥0:** export pole B / curtail A (v21b), bez `neg_sell_soc` shortfallu v buy&lt;0 slotech.
- **buy&lt;0:** tvrdě `ge=ge_pv=ge_bat=0` + měkký **`neg_buy_charge_shortfall`** (tlak na `bc_gi+bc_pv`).
- **sell&lt;0 + buy&lt;0:** žádný větev ventilu plné baterie → jen nabíjení/curtail.
**Ověření:** replan home-01 → tag v22; 11:0011:45 import+nabíjení, 12:15 bez exportu při buy&lt;0.
---
## 2026-05-28 — ranní sell&lt;0: držet SoC před buy&lt;0 (v21 / v21b)
**Problém (MCP run 16692, tag v20):** Od ~05:30 nabíjení z PV; v 09:15 už **98,3 %** SoC; od 09:15 masivní **export při sell&lt;0** (7 kW). V **11:0012:45** `buy&lt;0`, ale baterie plná → **žádný import**.
**v21:** `neg_sell_soc_underfill` / `pv_charge_shortfall` jen od `first_neg_buy_idx`; **`bc_pv=0`** před buy&lt;0 v sell&lt;0.
**v21b — Infeasible:** `bc_pv=0` + ventil **`w_pv_b_vent`** (export jen při plné baterii) → přebytek **pole B** (`pv_b` &gt; load) nemá kam (bilance). **Oprava:** před `first_neg_buy_idx` povolit **`ge_pv ≤ pv_b`** bez ventilu; safety `soc_max` u sell&lt;0 charge jen od `first_neg_buy_idx`.
**Tag:** `2026-05-28-morning-hold-soc-v21b`
**Ověření:** `scripts/diagnose_home01_infeasible.py`; replan home-01 → tag v `solver_params`.
---
## 2026-05-28 — revert tvrdých v19 constraintů (v20)
**Problém:** v19v19c opakovaně **Solver: Infeasible** na home-01 (ověřeno proti MCP run 16674 — `buy<0` od 11:00, ne 13:00). Vrstvené Python patch bez reprodukce na živých slotech.
**Rozhodnutí:** **Revert** celé v19 Python vrstvy (pre-neg discharge, `bc_pv=0` před buy&lt;0, neg-buy shortfall). Zůstává stabilní základ:
- **v17:** `bc_gi=0` při sell&lt;0+PV+buy≥0; `ge_pv ≤ pv_b` při sell&lt;0
- **v18:** večerní export push z DB `min(discharge, export)` W
Strategie před buy&lt;0 / import v buy&lt;0 patří do **SQL `R__063`** (masky `allow_*`), ne dalších tvrdých LP constraintů — až po feasibilitě na MCP datech.
**Tag:** `2026-05-28-revert-v19-hard-v20`
**Diagnostika:** `scripts/diagnose_home01_infeasible.py` + fixture z MCP `planning_interval` run 16674.
---
## 2026-05-27 (k) — Infeasible: soc na každém buy&lt;0 slotu + sell&lt;0 v pre-neg (v19c)
**Problém:** (1) **`neg_buy_soc_underfill`** na **každém** `buy<0` slotu vyžadoval `soc = soc_max` každých 15 min — při startu pod max fyzicky nemožné. (2) **`pre_neg_buy_discharge_ts`** mohlo zahrnout `sell<0` + `allow_discharge_export`**`ge_bat=0`** (sell&lt;0) vs **`z_export` → ge_bat≥1** → Infeasible.
**Oprava (tag `2026-05-27-pre-neg-buy-strategy-v19c`):**
- `neg_buy_soc_underfill` jen na **posledním** `buy<0` slotu horizontu.
- `pre_neg_buy_discharge_ts` jen při **`sell ≥ 1`** (ne SQL discharge maska se záporným sell).
- Třetí retry: **`relaxed_neg_buy_pressure`** (vypne měkké shortfall, ponechá `bc_pv=0` před buy&lt;0).
---
## 2026-05-27 (j) — Infeasible: pre-neg export mimo discharge_export_slots (v19b)
**Problém:** v19 přidávalo noční sloty (`sell ≥ 1`) do **`discharge_export_slots`** → režim **`w_arb`**: `bd` jen při exportu, ne k loadu → v noci nešlo pokrýt baseload → **Solver: Infeasible** (i po `relaxed_expensive_import`).
**Oprava (tag `2026-05-27-pre-neg-buy-strategy-v19b`):** `pre_neg_buy_discharge_ts` pouze povolí **`ge_bat`** (+ shortfall), **bez** rozšíření `discharge_export_slots`. Baterie dál může vybíjet k domu (`bd`) a paralelně exportovat (`ge_bat`).
---
## 2026-05-27 (i) — strategie před buy&lt;0: noční výboj, bez PV→bat, import v záporném nákupu (v19)
**Problém (home-01 run 16662, tag v18):** Večerní/ranní export OK. Zbývá: (1) noc jen ~500 W do domu, žádný `ge_bat` výboj před `buy<0`; (2) 08:4511:30 nabíjení z PV A do ~98 % ještě před `buy<0` (13:00); (3) v `buy<0` baterie plná → žádný import; (4) `neg_sell_soc_underfill` tlačilo na soc_max už v ranním `sell<0` okně.
**Oprava (tag `2026-05-27-pre-neg-buy-strategy-v19`):**
1. **Noční výboj:** `pre_neg_buy_discharge_ts` — shortfall + push `ge_bat` na site cap, bonus `z_export`, export podlaha `min_soc` (ne safety ramp).
2. **`bc_pv[t]=0` pro všechny sloty před `first_neg_buy_idx`** (i když `t in charge_slots` z `sell<0+PV`).
3. **`neg_sell_soc_underfill` jen po `first_neg_buy_idx`** — před záporným nákupem nehonit soc_max.
4. **`neg_buy_soc_underfill` + `neg_buy_grid_shortfall`** v `buy<0` slotech — tlak na soc_max a max `bc_gi` ze sítě.
**Ověření:** `pytest backend/tests/test_planning_dispatch_milp.py` — po deploy replan home-01: tag v19; noc `ge_bat` ~13,5 kW; před 13:00 SoC pod max; 13:0014:45 import + nabíjení k 100 %.
---
## 2026-05-27 (h) — export push z DB limitů, bez hardcoded 8000 W (v18)
**Problém:** `EVENING_BATTERY_EXPORT_MIN_W` a `PRENEG_MORNING_EXPORT_MIN_W` = 8000 W v kódu brzdily home-01 na 8 kW místo `site_grid_connection.max_export_power_w` (13,5 kW); u KV1 náhodou sedělo. `EVENING_PEAK_FULL_POWER_TOP_K = 6` arbitrární.
**Oprava (tag `2026-05-27-site-export-cap-from-db-v18`):**
- Smazány konstanty `EVENING_BATTERY_EXPORT_MIN_W`, `PRENEG_MORNING_EXPORT_MIN_W`, `EVENING_PEAK_FULL_POWER_TOP_K`.
- Helper `_battery_export_cap_w(battery, grid)` = `min(max_discharge_power_w, max_export_power_w)` z DB.
- Ranní/večerní push `ge_bat >= export_push_w * z_export` používá výhradně site limit (KV1 ~8 kW, home-01 ~13,5 kW).
**Ověření:** `pytest backend/tests/test_planning_dispatch_milp.py` — 87 passed (1 pre-existing).
---
## 2026-05-27 (g) — bc_gi=0 v sell<0+pv slotech, ge_pv≤pv_b při sell<0, evening top-K (v17)
**Problém v16 (run 16652):**
1. **Nákup ze sítě 18 kW v 09:1509:45 za buy 1,11,2 Kč:** R__063 přidává `allow_charge=true` i pro `sell<0+pv_surplus>0` (= "povolit PV nabíjení aby pole A nešlo do mínusu"), ale `t in charge_slots` v Pythonu pak otevřelo i `bc_gi` (grid→bat) za pozitivní buy → ztráta ~25 Kč.
2. **Export pole A v sell<0 oknu (11:0014:45):** `ge_pv` mohlo zahrnovat celý PV surplus, tj. pole A se mu vyhodil do mínusu za cenu až 1,08 Kč/kWh (~10 Kč ztráta na hodinu).
3. **Večerní prodej jen 8 kW místo 13,5 kW:** `EVENING_BATTERY_EXPORT_MIN_W = 8000` byl spodek tlaku — LP rozprostíral vybití do víc slotů místo zhuštění do peaků.
**Oprava (tag `2026-05-27-no-grid-charge-pos-buy-v17`):**
1. **bc_gi=0 v `sell<0+pv_surplus>0` slotech s buy≥0** (mimo `charge_slots` už zůstává). Důvod: `t in charge_slots` z PV důvodu **není** ekvivalentní "povolit nákup ze sítě". Arbitráž ze sítě (cheap buy → peak sell) zachována dokud `pv_surplus=0` (= test `test_vt_nt_cycle_evening_battery_sell`).
2. **ge_pv ≤ pv_b_forecast_w v `sell<0` slotech s pv_b > 0** (home-01: bez block_export). Pole A musí jít do baterie nebo curtail; pole B s green bonus 7,135 Kč → net 6+ Kč i při sell=1.
3. **Evening top-K full power push:** Top-6 nejvýnosnějších evening slotů má `ge_bat ≥ min(max_discharge, max_export)` (= 13,5 kW pro home-01). Aktivní jen pokud `len(evening_push_ts) ≥ 7` (= multi-slot peak okno, ne 1-slot regresní testy).
**Ověření:** `pytest backend/tests/test_planning_dispatch_milp.py` — 87 passed (1 pre-existing fail). Po deploy + replan home-01:
- 09:1509:45 **bez** import 18 kW (bc_gi=0).
- 11:0014:45 `curtail_a ≈ pv_a epsilon`, `ge_pv ≤ pv_b`.
- Večerní peak (20:30, 20:45, 21:00, 22:00) **ge_bat ≥ 13 500 W** → kratší okno, vyšší marže.
---
## 2026-05-27 (f) — zjednodušená strategie pro buy<0 okno (v16, revert v14+v15)
**Problém v14/v15 (run 16622, 16636, 16642):** Vrstvy soft penalty (cap+slack, PV charge suppressed penalty) LP **nedonutily** vybít baterii ani omezit PV pumping. LP přijímal sloupec slack 24 kWh × 50 Kč/kWh = 1190 Kč a baterii nabíjel z ranního PV (10:30 SoC=95 %), pak v `buy<0` okně (13:0014:45) curtail pole A 59 kW + export pole A do mínusu.
**Strukturální root cause (3 vrstvy):**
1. R__063 `allow_charge=false` ze SQL Pythonský `solve_dispatch` ignoruje pro PV charging (`bc_pv ≤ pv_surplus` i pro `t not in charge_slots`).
2. `discharge_export_slots` v noci `false` (R__063) → LP nemá cestu jak baterii vybít přes ge_bat.
3. `acquisition` v LP je vstupní konstanta — LP nevidí, že buy<0 okno je „lepší cesta" než ranní PV pumping.
**Oprava (tag `2026-05-27-simple-buy-neg-window-v16`):** Reverted v14+v15, znovu postaveno **2 jednoduchá pravidla** podle business logiky:
1. **Tvrdé `bc_pv[t] = 0` pre-first_neg_buy_idx** (slots kde `t not in charge_slots`): PV poteče do gridu (sell≥0) nebo curtail, ne do baterie. R__063 už pro `sell<0+pv_surplus` přidává `allow_charge=true` (= `t in charge_slots`), takže pole A v `sell<0` slotech může nabíjet baterii (= nevyhodit do mínusu).
2. **Rozšíření `discharge_export_slots`** o pre-`buy<0` sloty se **dynamickým prahem** `sell ≥ max(avg(buy<0) + degradation_cost, 0.1) Kč/kWh`. Pro home-01 (avg buy<0 ≈ 0,22, degrad ≈ 0,15) to dělá práh ~0,1 Kč → prakticky všechny noční sloty se `sell > 0`. Ekonomická logika: marže `sell_t acquisition_in_neg_buy_window degradation`, a pokud `acquisition ≈ záporný` (buy<0 v okně), je výhodné vybít a znovu nabít i za sell ~1 Kč/kWh.
**Business logika (od uživatele):**
- Noc před `buy<0`: vybít baterii za sell ~3 Kč/kWh.
- Ráno: minimální SoC.
- `buy<0` okno: PV B necurtailovat (R__063 už řeší), nabíjet ze sítě (LP samo, buy záporný = `t in charge_slots`).
- Po `sell>0`: baterie plná, max prodej.
- Večer: prodat zbytek.
**Ověření:** `pytest backend/tests/test_planning_dispatch_milp.py tests/test_planning_charge_slot_selection.py` — 87 passed (1 pre-existing fail nesouvisí). Po deploy MCP: `select pr.solver_params->'planner_build_tag'` = `…-v16`, plán home-01 25.5.: SoC v 12:45 < 50 %, 13:0014:45 SoC roste z capu k ~95 %, `pv_a_curtailed_w` blízko 0 v okně.
---
## 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:0014: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:3014: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:0014:45 SoC roste (PV charging), 13:30 `grid_setpoint` < 0 jen pole B (curtail pole A = 0), bilance: `cashflow_czk(13:0015: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`, `|acq1acq2| < 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:3023: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 &lt; 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** &lt; 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 1634616350, tag v7):** KV1 0608 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&lt;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í **0004** 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 511 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 0004 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:4505: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 (0006) 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:0008: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ší 12 kW
**Problém:** Run **15838** — 06:15 Prague ~6,1 kW, 06:3007:30 ~1,42,2 kW, 07:4508: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:3007:30 ~12 kW, **07:4508: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 ř. 787791).
- 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í:**
```