# 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-06-12 — EV účtování v2: headroom, deadline boundary, min. výkon WB, via-bat reporting

**Problém (hloubková diagnóza EV):** (a) „nenabíjet" (nízký target) oportunismus
nevypnul a paradoxně ZVĚTŠIL headroom (= 100 − target); session bez mandátu
(nebo žádná session) při `buy < 0` pumpovala energii bez stropu; (b) off-by-one
v deadline sumě (`range(t_dl + 1)` — slot začínající v deadline se počítal „do
deadline"); (c) reporting lhal: `battery_arbitrage_czk` konstantně 0, via_bat se
nepropagoval do bundle, UI cenilo EV kWh z baterie slotovým buy; (d) split
ev_direct/via_bat byl arbitrární (direct nesvázán s gi + PV); (e) min. výkon
wallboxu (`asset_ev_charger.min_power_w`, 1380 W = 6 A) ignorován → setpointy
400–900 W nevykonatelné.

**Mechanismus:** headroom z `max(target_soc_pct, soc_at_connect_pct)` a
opportunistic_value = `coalesce(session, vehicle)` v `fn_planning_site_context`
(V099: `ev_session.opportunistic_value_czk_kwh`, NULL = zdědit, 0 = vypnout;
patch přes `fn_ev_session_apply_patch`, validace ≥ 0); solver_v2: deadline suma
`range(t_dl)`, bez session EV == 0, dekompozice `total == needed − unmet + opp`
i pro needed = 0; binárka `ev_on` → setpoint ∈ {0} ∪ [min_power_w, max]
(min_power_w nově ve vehicles JSON kontextu); `Σ ev_direct ≤ gi + pv_a_net +
pv_b_eff`; `battery_arbitrage_czk` = via_bat kWh × oportunitní cena (min sell
exportního slotu téhož pražského dne, jinak terminal value, clamp ≥ 0);
`fn_plan_current_bundle.intervals` + `ev1/ev2_via_bat_w`. **Oportunismus PO
deadline zůstává POVOLENÝ** (rozhodnutí: auto často doma, odjezd řeší rolling
replan). Fixtures: `extract_fixtures.py --keep-ev` (default dál EV nuluje).

**Soubory:** `V099__ev_session_opportunistic.sql`, `R__039_fn_planning_site_context.sql`,
`R__015_fn_ev_session_patch.sql`, `R__033_fn_plan_current_bundle.sql`,
`services/planning/solver_v2.py`, `services/planning/db_io.py`,
`scripts/harness/extract_fixtures.py` + README, `docs/04-modules/ev-charging.md`,
`docs/04-modules/planning.md`.

**Ověření:** `tests/test_solver_v2.py` +7 (deadline boundary, stop-session,
no-session při buy<0, direct ≤ gi+pv, setpoint ∈ {0}∪[1380, max], opp po
deadline > 0, battery_arbitrage_czk > 0 u via_bat); golden gate beze změny
snapshotů (v1 nedotčen, fixtures bez EV); `solver_v2_eval.py` před/po identický
(CELKEM −1283.5 Kč, Δ −221.9 vs v1); plná sada 310 passed / 4 xfailed.

---

## 2026-06-12 — idle-skip telemetrie: TUV delta normalizovaná na °C/min

**Problém:** telemetry_collector nově přeskakuje 1min zápisy idle zařízení
(heartbeat 840 s — viz `telemetry.md`, sekce Idle-skip zápisů). Vstupy
plánovače čtené z těchto tabulek nesmí předpokládat hustou 1min řadu.

**Mechanismus:** `fn_update_tuv_usage_stats` (R__018) počítá deltu TUV jako
`(temp − lag(temp)) / gap_min` (°C/min, mezery > 30 min vyloučeny) — pro
hustá 1min data numericky identické s původním per-row LAG; po idle-skip bez
až 14× nadhodnocení delty. Ostatní vstupy solveru (poslední TUV teplota v
`fn_planning_site_context`, poslední EV status v `fn_load_planning_slots_full`,
baseline stats) pokrývá heartbeat beze změny. Audit: EV/TČ `sum/15` v R__019.

**Soubory:** `telemetry_collector.py`, `R__018_fn_extended_planning.sql`,
`R__019_fn_fill_audit_interval.sql`, `R__097_vw_pool_pump.sql`, `PoolCard.tsx`,
`docs/04-modules/telemetry.md`.

**Ověření:** `tests/test_telemetry_idle_skip.py` (změna/aktivita/heartbeat/
start; EV arrival přežije skip i restart procesu); celá sada 303 passed.

---

## 2026-06-12 — v2 AKTIVNÍ v produkci + robustnostní trojice „nejistota jako cena"

**Přepnutí (847015f):** `PLANNING_ENGINE_VERSION` default **v2** v deploy compose; v1 běží
jako shadow peer. První živé srovnání (11. 6. večer): v1 kvůli relax řetězci potlačil
evening push a nedoprodal špičku 3.92 — v2 prodal na 13.5kW stropu, o 28.8 Kč lépe.
Rollback: `PLANNING_ENGINE_VERSION=v1` v `/opt/ems-deploy/.env`.

**Trojice mechanismů proti chybě predikce (vše ceny, ne okna; parametry v DB):**

| Mechanismus | Chrání před | Implementace |
|---|---|---|
| **Noční SoC polštář** (e464b11) | chyba predikce noční spotřeby → drahý noční nákup | soft floor `min_soc + night_baseload_buffer_wh[t]` (R__063, klesá k 0 do rána); porušení placené buy cenou slotu. Bonus: těsnější LP zrychlil extrémní fixtures z 10 s na 0.3–2.6 s |
| **PV-risk front-load** (2932d48) | večerní mrak v okně sell<0 (v1 řešil rampou) | prémie za držení energie dřív v neg slotech; `asset_battery.planner_pv_risk_frontload_czk_kwh` (V090) |
| **Denní SoC rampa** (e0410f9) | nenadálý odběr za kladných cen (KV1 ráno 11 % a prodává) | deficit pod `safety_soc_target_wh` (R__063 rampa reserve→reserve+noc, 6–19 h) platí nájem `buy×faktor`/slot; `planner_safety_soc_risk_factor` (V091, default 0.05) |

Eval gate po každém kroku: v2 lepší než v1 na všech fixtures (+221.9 Kč) drženo.
Solver testy 17; plná sada 274 passed / 4 xfailed (+1 předexistující reg340).

**EV spotřební forecast (4095f0f):** týdenní rytmus vozidla (odometer+SoC při
příjezdu/odjezdu z Tesla API, žádné buzení) → `ev_usage_stats` per DOW →
`fn_ev_required_soc` / `fn_ev_next_departure` → target+deadline session
(za flagem `target_soc_forecast_enabled`). Detail: `docs/04-modules/ev-charging.md`.

**Zimní posouzení:** vlastní zimní data neexistují (telemetrie od 3/2026); 2 zimy raw
OTE: spready 2.1–3.2 Kč (vs jaro 4–5.2), neg dny ~0 → klíč je TČ track. v2 bez
sezónních oken (v1 měl 17h/5–11h/AM-PM = jarní slunce).

---

## 2026-06-11 — Refaktor „Čistý plánovač“: harness, dekompozice, solver_v2 (Fáze 0–3)

**Kontext:** Ekonomický audit potvrdil systémový problém heuristické vrstvy: na neg-sell dnech Σ heuristických penalt v objective 13× převažuje reálný cashflow; GAP actual vs perfect-hindsight oracle za 29 dní home-01 = **2 185 Kč ≈ 27 %**. Plný plán a stav: `docs/refactor-clean-planner.md`.

**Fáze 0 — harness (`scripts/harness/`, `backend/tests/golden/`):**
- `extract_fixtures.py` (vstupy solveru z DB → JSON), **golden replay gate** `test_golden_replay.py` (bit-perfektní diff, `GOLDEN_UPDATE=1` jen vědomě), `economics_report.py` (actual vs oracle, SoC-adjusted), `penalty_audit.py`.
- 6 fixtures vč. **`home-01_2026-05-01_extreme_neg_buy`** (buy −13,26): v1 **Infeasible po všech 8 relax krocích** — zmrazeno jako golden failure snapshot.

**Fáze 1 — dekompozice:** `planning_engine.py` 6 345 → 3 960 ř.; nový balíček `services/planning/` (`constants` — všech 59 konstant vč. penalt, `types`, `forecast`, `db_io`, `heuristics` — 88 pre-solver funkcí). Engine = fasáda, importy beze změny, golden gate zelená po každém kroku.

**Fáze 2 — audit:** 16 z 26 ekonomických penalt **mrtvých** na všech fixtures (vč. `EVENING_PUSH_Z_EXPORT_BONUS` na evening-push dni); aktivní penalty silně interagují. 4 trvale failující testy = **stale** (chování před retry-chain v5) → `@unittest.expectedFailure` se zdůvodněním; suite poprvé zelená (120 passed, 4 xfailed).

**Fáze 3 — `services/planning/solver_v2.py` (čisté jádro):**
- Objective = cash + degradace − terminal SoC value (DB faktor); tvrdá pravidla (bilance, breaker, curtail jen A, GEN cutoff binárka, neg-buy/neg-sell bloky, export z baterie ⇒ arb floor, zákaz souč. imp+exp), EV deadline s placeným slackem (50 Kč/kWh), TUV look-ahead, režimy. **SQL masky `allow_*` ignoruje** (heuristika, ne fyzika).
- **Výsledky (`solver_v2_eval.py`):** lepší než v1 na všech 5 řešitelných fixtures (**+231,5 Kč ≈ +22 %**, SoC-fér); extreme_neg_buy den v1=INFEASIBLE → v2 OK. Časy 0,4–10 s (2 extrémy na time limitu — TODO méně binárek).
- **Router verzí:** `_solve_dispatch_for_version` v engine; env `PLANNING_ENGINE_COMPARE_ENABLED=true` = shadow (v1 aktivní, v2 peer, diff v `planning_run.solver_params.comparison`); `PLANNING_ENGINE_VERSION=v2` = přepnutí. Default v1 — beze změny chování.

**Soubory:** `services/planning/*`, `planning_engine.py`, `tests/test_solver_v2.py` (11), `tests/test_golden_replay.py`, `scripts/harness/*`.
**Ověření:** plná sada 245 passed / 4 xfailed (1 předexistující reg340 fail); golden 7/7; `solver_v2_eval.py`.

---

## 2026-06-06 — Pozdní replan večer: Infeasible při vysokém SoC (home-01)

**Problém:** Po přepnutí na AUTO a ručním replanem (~21:00, SoC **~74 %**, zítra `buy<0` + `sell<0`): všechny retry včetně `neg_sell_phases_fallback` → **`Solver: Infeasible`**. Aktivní zůstal starý plán z 17:00 (import místo večerního vývozu k **reserve ~20 %**).

**Příčina:** SQL maska `allow_charge=false` ve večerních slotech (drahý `buy`, `sell` < `buy`) + guard drahého importu vyžadoval baseload z baterie (`bd`), zatímco **v64 `future_neg_buy_discharge`** současně vynucoval večerní vývoz — LP bez rozšíření `charge_slots` neměl řešení.

**Oprava (tag `2026-06-06-home01-late-replan-infeasible-v1`, doplněno **v2**, guard **v3**):**
- Při **`future_neg_buy_discharge`**: rozšířit `charge_slots` o večerní / exportní sloty dne replanu (grid smí krmit load během vývozu).
- **`_unlock_late_replan_evening_slots`** po `fn_load_planning_slots_full` — večer D0 `allow_charge` + export z DB.
- Nový retry **`relaxed_pos_sell_ge_block`** (+ **`relaxed_solver_masks`** nouzový) v `SOLVER_RELAX_STEPS`.
- **v2:** two-pass pass2 dědí všechny relax flagy; při pass2 Infeasible fallback na pass1; override push zrušen už od `relaxed_neg_prep_hold_only`.
- **v3 (`degraded-night-guard-v3`):** v **`relaxed_solver_masks`** — spot **bez nočního/večerního `ge_bat` exportu**; **`_degraded_relaxed_night_self_consume_indices`** + tvrdý expensive-import guard (dům z baterie až **`min_soc`**, ne import za ~4 Kč); exportní podlaha SoC ≥ **`reserve_soc`**.
- **v4 (`degraded-night-guard-v4`):** oprava v3 — nouzový režim pro `relaxed_solver_masks` (viz výše).
- **v5 (`strict-late-replan-v5`):** **strict solve** bez relax chainu při pozdním replanu večer před `buy<0` dnem — `late_replan_strict_active`: večer 17–22h vývoz k **reserve**, noc self-consume (discourage import), vypnutí neg-evening bundle + prep fází + tvrdého evening push; snapshot `strict_late_replan_*_ts`, `late_replan_solver_relax`.

**Soubory:** `planning_engine.py`, `scripts/repro_home01_23840.py`, testy `test_home01_late_replan_high_soc_realistic_masks`, `test_degraded_relaxed_solver_evening_to_reserve_and_night_self_consume`.

**Ověření:**
- `PYTHONPATH=backend python3 scripts/repro_home01_23840.py` → `OK two_pass`
- `pytest backend/tests/test_planning_dispatch_milp.py -k "home01_late_replan or degraded_relaxed"`
- Po deployi v5: `relax_chain = ["strict"]`, `late_replan_strict_active = true` — **bez** `relaxed_solver_masks`; večer export, noc bez drahého importu baseloadu.

---

## 2026-06-06 — BA81 GEN cut-off exekuce při sell&lt;0 (Branch 4)

**Problém:** Audit BA81 6. 6. 2026 (07:45–08:30, `sell<0`): plán `grid_setpoint_w=0`, `deye_gen_cutoff_enabled=false`, ale **`actual_grid_export_wh` > 0** a **`flow_pv_to_grid_wh` > 0** (~0,8–1 kW). Reg **145** (`export_ban`) nestačí — mikroinvertory na GEN portu exportují, dokud reg **178** bits 0–1 ≠ cut-off ON.

**Příčina:** Solver nechal `z_gen_cutoff=0` (PV B jen do domu v bilanci); exporter zapínal MI cut-off jen z plánového flagu, ne z `export_ban`.

**Oprava (tag `2026-06-06-ba81-gen-cutoff-exec-v1`):**
- **LP:** `z_gen_cutoff[t]==1` při `sell<0` a zakázaném vývozu (fixní tarif, `block_export_on_negative_sell`, `block_pv_export_neg_sell`, nebo `buy<0`+`sell<0`).
- **Exekuce:** `deye_mi_export_cutoff_want_enabled()` — cut-off ON při `export_ban` nebo plánovém flagu; `_passive_no_export_guard` nastaví `deye_gen_cutoff_enabled=True`.

**Soubory:** `planning_engine.py`, `deye_helpers.py`, `inverter.py`, `setpoints.py`.

**Ověření:**
- `pytest backend/tests/test_planning_dispatch_milp.py -k "fixed_tariff_neg_sell or gen_cutoff"`
- `pytest backend/tests/test_control_exporter_tou.py backend/tests/test_control_export_plan_guard.py -k "mi_export or neg_sell"`
- MCP po deployi (BA81, `sell<0`): `deye_gen_cutoff_enabled=true`, `actual_grid_export_wh≈0`; `modbus_command` reg **178** s MI bits = 3 nebo verify skip jen pokud už cut-off ON.

---

## 2026-06-06 — Dynamický terminal SoC factor při future neg buy (v65, Branch 5)

**Problém:** Binární × **0,1** při **`future_neg_buy_discharge`** (v64) neškáloval s vzdáleností a záporností **`buy<0`**; **`planner_terminal_soc_value_factor = 0.9`** na home-01 držel baterii i když v horizontu bylo levnější nabíjení.

**Změna (v65):**
- **`terminal_neg_buy_weight`** (`w_neg`): `effective_factor = planner_terminal_soc_value_factor × (1 − w_neg)`; `w_neg` roste s blízkostí prvního **`buy<0`** (horizont 36 h) a magnitudou záporné ceny (ref 1 Kč/kWh).
- Odstraněn pevný **`FUTURE_NEG_BUY_TERMINAL_SOC_FACTOR_MULT`**; váha platí vždy, když je v horizontu **`buy<0`**, ne jen při **`future_neg_buy_discharge`**.

**Soubory:** `backend/services/planning_engine.py`, `backend/tests/test_planning_dispatch_milp.py`.

**Ověření:**
- `pytest backend/tests/test_planning_dispatch_milp.py -k "terminal_soc or terminal_neg_buy"`
- MCP: `solver_params->'inputs'->>'terminal_neg_buy_weight'` > 0 před dnem s **`buy<0`**; `terminal_soc_factor_effective` < `planner_terminal_soc_value_factor`.

---

## 2026-06-06 — Future neg-buy večerní export (v64, Branch 2)

**Problém:** home-01 run 23784 při **`relaxed_neg_prep_window`**: `evening_push_hard_suppressed`, prázdné **`neg_evening_push_slots`**, **`pos_sell_pre_neg_buy_ts`** blokoval `ge_bat` ve večerní špičce, terminal SoC shadow price držel ~80 % SoC + import @ ~5 Kč.

**Změna (v64):**
- **`future_neg_buy_discharge`**: před dnem s **`buy<0`**, pokud FVE v **`sell<0`** pokryje deficit do prep rampy, zůstává neg-evening bundle (push + kotvy **`reserve_soc`**) i při **`relaxed_neg_prep_window`** (strict pre-neg PV export bundle se vypne).
- **`evening_push_hard_suppressed`** jen při **`neg_sell_phases_fallback`**, ne při **`relaxed_neg_prep_window`**.
- **`pos_sell_pre_neg_buy_ge_exempt_slots`**: večerní peak před **`buy<0`** nesmí dostat `ge=0`, pokud je vývoz ekonomicky výhodný.
- **`terminal_soc_factor_effective`**: v64 binární × **0,1** při **`future_neg_buy_discharge`** — nahrazeno v65 dynamickým **`terminal_neg_buy_weight`** (viz výše).

**Soubory:** `backend/services/planning_engine.py`, `backend/tests/test_planning_dispatch_milp.py`.

**Ověření:**
- `pytest backend/tests/test_planning_dispatch_milp.py -k "future_neg_buy or relaxed_neg_prep"`
- MCP: `solver_params->'inputs'->>'future_neg_buy_discharge' = true`, `evening_push_hard_suppressed = false` (bez fallback), večer `grid_setpoint_w < 0` k ~**`reserve_soc`**.

---

## 2026-06-06 — Infeasible journal + granulární prep relax (v63, Branch 1)

**Problém:** home-01 run 23784 prošel až **`relaxed_neg_prep_window`** (3. retry) → `evening_push_hard_suppressed`, prázdné `neg_evening_push_slots`, SoC ~80 % ve špičce + import @ ~5 Kč. Selhání **`Solver: Infeasible`** se neukládalo do DB (jen log backendu).

**Změna (v63):**
- Nový krok **`relaxed_neg_prep_hold_only`**: uvolní jen `prep_soc_shortfall` + prep hold binárky; **neg-evening bundle a tvrdý evening push zůstávají**.
- **`relaxed_neg_prep_window`** až jako 4. krok (full prep relax včetně neg-evening a `evening_push_hard_suppressed`).
- **`PlannerSolverError`** + `relax_chain` ve snap; po vyčerpání retry → **`fn_planning_run_fail`** (`status=failed`, `error_text`, migrace **V084**).
- **`scripts/diagnose_home01_infeasible.py`**: `--print-export-sql`, bisect všech relax kroků.

**Soubory:** `backend/services/planning_engine.py`, `db/migration/V084__planning_run_failed_status.sql`, `db/routines/R__091_fn_planning_run_fail.sql`, `scripts/diagnose_home01_infeasible.py`, `backend/tests/test_planning_dispatch_milp.py`.

**Ověření:**
- `pytest backend/tests/test_planning_dispatch_milp.py -k "prep_hold or relax_chain or evening_push_override"`
- MCP po neúspěšném API replanu: `select id, status, error_text, solver_params->'relax_chain' from ems.planning_run where site_id=2 and status='failed' order by created_at desc limit 3;`
- Úspěšný rolling: `relaxed_neg_prep_hold_only: true` bez `relaxed_neg_prep_window` a `evening_push_hard_suppressed: false`.

---

## 2026-06-06 — charge-slot budget v1 (Branch 3: BA81/KV1)

**Problém:** v58 `sell > min_sell + 0,20 → bc_pv = 0` držel denní SoC ~60 % při slunci (konflikt s `R__063` vrstvou A). Fixní lokality neměly večerní push podle `sell > buy + spread`.

**Změna:**

1. **`R__063_fn_load_planning_slots_full.sql`:** nové sloupce `charge_target_wh`, `pre_window_wh`, `in_window_wh`, `charge_slot_wh`, `charge_cum_wh`, `charge_layer`, `charge_slot_reason`; PV vrstva A u **fixed** řazena **`sell ASC`** + Wh kumulace (spot dál `store_score DESC`).
2. **`planning_engine.py`:** odstraněn v58 (`fixed_high_sell_no_pv_charge`, `fixed_grid_charge_unprofitable`); LP respektuje jen `allow_charge` / `allow_grid_charge` ze SQL. Večerní push u fixního tarifu: **`sell > buy + spread`** (`fixed_evening_push_sell_above_buy`); KV1 zachovává v52 morning-peak pravidlo.
3. **`solver_params.charge_slot_budget`** — audit rozpočtu na aktivním runu.

Tag **`2026-06-06-charge-slot-budget-v1`**.

**Ověření:**

```bash
pytest backend/tests/test_planning_charge_slot_selection.py backend/tests/test_planning_dispatch_milp.py \
  -k "fixed_high_sell or fixed_tariff_evening or fixed_evening or kv1_evening" -q
```

MCP: `planning_run.solver_params->'charge_slot_budget'`; u BA81 večer `evening_push_ts` neprázdné při `sell > buy`.

**Zbývá (Branch 4–5 spec):** změkčení v44 grid před neg; plná náhrada v33 cushion přes `pre_window_wh` frontu u home-01.

---

## Plánováno — charge-slot-budget home-01 (pre-neg fronta, v44)

**Stav:** Branch 3 hotový pro fixed; spot pre-neg fronta a v44 změkčení — viz [`planning-charge-slot-budget.md`](04-modules/planning-charge-slot-budget.md) §6.

---

## 2026-06-01 — KV1: noc z baterie, ne import za 6,35 Kč (v62)

**Problém:** Po večerním vývozu (~32 % SoC) plán **22:00–06:00** krmil dům ze **sítě** (`grid ~260 W`, `bat 0`) místo z baterie. Fixní **buy ≈ charge_acquisition ≈ 6,35** → `expensive_import_slot` nikdy true → neplatilo `bd ≥ load` ani noční penalizace importu (`buy > acq` je false).

**Změna (v62):** u **`purchase_pricing_mode=fixed`**: `expensive_import_slot = true` (buy ≥ 0); `_night_self_consume_discourage_import_indices` zahrne noční sloty i při **buy = acq**.

Tag **`2026-06-01-kv1-fixed-night-self-consume-v62`**.

---

## 2026-06-01 — home-01: grid jen při buy ≤ acquisition (v61, zrušeno v60)

**Problém:** **19:00** nabíjení za **buy ~5,5** při **`charge_acquisition ~3,25`** z rána → falešně ziskový večerní export. **v60** (`sell < buy` ve slotu) bylo **špatně**: u spotu (a často u fixního tarifu) je **`sell < buy` normální** (marže distributora) — arbitráž je **mezi sloty**, ne v jedné čtvrthodině.

**Změna (v61):** spot: **`bc_gi = 0`** jen když **`buy[t] > charge_acquisition + degrad`** (nákup v drahém slotu, ne levný NT). Export/push: zpět **`sell > acq + degrad`**. Fixní: **`bc_gi`** dál jen **`sell > min_sell_horizon`** (v59 SQL min sell); bez pravidla `sell < buy`.

Tag **`2026-06-01-spot-grid-charge-at-acq-buy-v61`**.

**Ověření:** `pytest … -k spot_no_grid_charge_when_buy_above_acquisition`.

---

## 2026-06-01 — BA81/KV1: zákaz grid nabíjení mimo min sell + večer bez charge (v59)

**Problém (po v58):** **KV1** po večerním vývozu **22:00–22:15** nabíjel ze sítě za **buy ~6,35 Kč** (`allow_charge` z `R__063` podle `slot_ord`, ne nejnižší sell). **BA81** v špičce **sell ~9,6** částečně **nabíjela** (`allow_charge` + PV), místo čistého vývozu; **03:30** grid nabíjení před východem slunce.

**Příčina v58:** `bc_gi = 0` jen při `pv_surplus > 500 W` — v noci prázdné, grid nabíjení projde.

**Změna (v59):**
- LP: **`fixed_grid_charge_unprofitable`** — `bc_gi = 0` když `sell < buy + degrad` **nebo** `sell > min_sell + 0,20` (bez podmínky na FVE).
- LP: v **`evening_push_ts`** při `sell > buy` → `bc_pv = bc_gi = 0` (jen vývoz).
- **`R__063`:** u fixního tarifu grid sloty jen kde `sell ≤ min(sell≥0) + degrad + 0,05`, řazení **`sell_price ASC`** (ne `slot_ord`).

Tag **`2026-06-01-fixed-grid-charge-min-sell-v59`**.

**Ověření:** `pytest … -k "fixed_no_grid_charge or fixed_evening_push_no_charge"`; po deployi Flyway + backend replan KV1/BA81.

---

## 2026-06-01 — BA81/KV1: FVE export při vysokém sell, nabíjení u min sell (v58)

**Problém:** U **`purchase_pricing_mode=fixed`** (BA81 buy ~3,09 Kč, KV1 ~6,35 Kč) oproti home-01 (spot):
- půlnoc→ráno baterie **~24–30 %** bez vývozu i tam, kde `sell > buy + degrad` (BA81 úsvit ~3,35 Kč);
- **~06:00** nabíjení z FVE při výkupní **~3 Kč/kWh**, místo exportu přebytku a nabíjení až u **nejnižšího sell** v horizontu (**~1,5 Kč**, poledne);
- KV1 večer jen malý vývoz ve špičce, ráno zbytečně vysoká rezerva.

Příčiny v LP: chybějící **`bc_pv == 0`** při sell výrazně nad denním minimem; `pv_store` / `ge_pv == 0` u fixního tarifu mimo úzké `fixed_pre_neg_*`; **`evening_early_export_ban`** (`ge_bat = 0`) i pro profitable `sell > buy` v noci; `peak_export_shortfall` noční okno přeskakoval (pravidlo pro spot / `evening_push`).

**Změna (v58)** — `backend/services/planning_engine.py`:
- Konstanta **`FIXED_PV_CHARGE_ONLY_NEAR_MIN_SELL_CZK_KWH = 0.20`**; **`fixed_horizon_min_sell_pre`** = `min(sell_price)` pro `sell ≥ 0` v horizontu.
- **`fixed_high_sell_no_pv_charge`:** `sell > min_sell + 0,20` a PV přebytek > `NIGHT_EXPORT_PV_SUNRISE_SURPLUS_W` → **`bc_pv = 0`**, **`bc_gi = 0`**; rozšířen **`skip_pv_store_block`** (FVE do sítě, ne do baterie).
- Fixní tarif: **`evening_export_exempt_ts |= profitable_export_ts_pre`** (`_slot_profitable_battery_export`: `sell > buy + degrad`).
- **`peak_export_shortfall`:** v nočním okně pokračovat i pro fixní profitable sloty (ne jen tvrdý `evening_push`).
- Snap: `fixed_horizon_min_sell_czk_kwh`, `fixed_pv_charge_near_min_sell_margin_czk_kwh`.

**Soubory:** `backend/services/planning_engine.py`, `backend/tests/test_planning_dispatch_milp.py`, `docs/04-modules/planning.md`, `docs/planning-changelog.md`.

Tag **`2026-06-01-fixed-pv-export-min-sell-charge-v58`**.

**Ověření:**

```bash
cd backend && pytest tests/test_planning_dispatch_milp.py \
  -k "fixed_high_sell_no_pv_charge or fixed_night_profitable" -q
```

MCP po replanu BA81/KV1: `planning_run.solver_params->>'planner_build_tag'` obsahuje `v58`; ráno export (`grid_setpoint_w < 0`, `battery_setpoint_w` malé), poledne nabíjení (`battery_setpoint_w` vysoké u min sell).

---

## 2026-06-01 — home-01: večerní vývoz po relaxed_expensive_import (v57)

**Problém:** v55 při **jakékoli** relaxed větvi vynulovalo `evening_push_ts` → `evening_early_export_ban` zakázal `ge_bat` i při sell **~9,6 Kč/kWh**; baterie jen samospotřeba, zítra export FVE za **~2 Kč**.

**Změna (v57):** `evening_push_ts` se **nemazá** při `relaxed_expensive_import` / `relaxed_neg_buy_charge`; tvrdý push jen při `relaxed_neg_prep_window` / `neg_sell_phases_fallback` (`evening_push_hard_suppressed`). Fallback: alespoň jeden večerní peak slot. Snap: `evening_push_hard_suppressed`, `evening_push_peak_fallback_used`.

Tag **`2026-06-01-evening-push-keep-on-relaxed-import-v57`**.

---

## 2026-05-31 — home-01: ranní tvrdý export + pass2 (v56)

**Problém:** Po v55 stále **`422 Solver: Infeasible`** u ručního replanu. Příčina: tvrdé **`ge_bat` push** v `morning_pre_neg_export_ts` zůstávalo aktivní i při `relaxed_*` (25 % SoC + neg den 31.5. → nelze exportovat ráno a zároveň splnit prep). Pass2 two-pass mohl spadnout i když pass1 prošel.

**Změna (v56):** tvrdý ranní/pre-neg export **jen bez** `any_relaxed`; pass2 při Infeasible **vrátí pass1**. Snap: `morning_pre_neg_export_hard`, `any_relaxed_solve`, `two_pass_pass2_infeasible_used_pass1`.

Tag **`2026-05-31-morning-export-relaxed-v56`**.

---

## 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 1–2 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 5–11 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:00–18: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:00–03:15** ranní peak export → SoC **~13,5 %** (pod **reserve 20 %**). **05:15–06:00 Prague** (= 03:15–03:45 UTC) plán **řeže celou PV A** (`curt_a = pv_a` při ~86–346 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 (v45–v46):** 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:45–08: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:45–07:30** grid+bat nabíjení za **~2,6–3,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 **11–16** (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 **02–06h** (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 02–06h.
- **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,5–13,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 **peak−degrad** (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,5–13,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, v32–v35, 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 `max−0,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 `peak−0,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 **15–25 %** 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.5–1.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_*` (D−1 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_neg−1` + výboj ve **všech** kladných sell slotech před 1. `sell<0` (ne jen D−1 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` 12 800), 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 D−1 ≤ **`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 D−1** (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** + **0–5h** 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_discharge−load)/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_discharge−load)`; 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 19–21 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_discharge−load)/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 (≥ max−0,05): **plný** `ge_bat`; **počet slotů** = Wh rozpočet, řazení `sell` desc | Není jediný slot; není široké peak−degrad 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:30–07:00, buy≥0): nabíjení z PV/sítě místo přípravy kapacity.
- **Při buy&lt;0** (12:15–12: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:00–11: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:00–12: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:** v19–v19c 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:45–11: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:00–14: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:15–09:45 za buy 1,1–1,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:00–14: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:15–09:45 **bez** import 18 kW (bc_gi=0).
- 11:00–14: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:00–14:45) curtail pole A 5–9 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:00–14: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: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 &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 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&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í **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í:** …
```
