Files
ems/docs/planning-changelog.md
Dusan Vojacek a32839bf67 feat(planner): EV anti-fragmentace + 3f power floor (Fix B)
3f floor (phases>=3 → 6A×fáze×230 ≈4140W, ruší 1f trickle) + block-start penalta
(asset_ev_charger.planner_ev_start_penalty_czk V108, default 0=no-op). Golden gate
zelená (363 passed). Postaveno paralelním worktree agentem, zvalidováno sériově.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:55:17 +02:00

1330 lines
105 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-06-14 — EV anti-fragmentace + 3f power floor (Fix B, solver_v2)
- **Problém:** EV nabíjení v solveru spojité po slotech bez start/stop penalty → rozsekané přes nesouvislé sloty + dílčí 1f trickle (sub-6A, který control stejně shazoval na 0 A) → cyklování nabíječky, Tesla notifikace.
- **Mechanismus (fix):** (a) **3f power floor** — pro `asset_ev_charger.phases >= 3` je min nabíjecí dávka 6 A × fáze × 230 V (≈4140 W) místo 1f ~1380 W (strop = max výkon vozidla); ruší sub-6A 1f drobky (fyzikálně realizovatelné dávky). (b) **block-start penalta** — per-slot binárka `ev_on`, hrana `ev_start[t] >= ev_on[t]ev_on[t1]`, objektiv += Σ ev_start × `asset_ev_charger.planner_ev_start_penalty_czk` (V108, **default 0 = no-op**, kalibruje se per wallbox). Drží v2 filozofii „nejistota/opotřebení = cena".
- **Soubory:** V108, R__039 (phases + start_penalty do kontextu), db_io.py, constants.py, solver_v2.py.
- **Ověření:** golden gate 7 passed + full suite 363 passed (fixtures EV nulují → start penalta inertní). Živě ověřeno: `asset_ev_charger.phases=3`, `min_power_w=1380` (1f) → 3f floor opraví na 4140. **Pozn.:** 3f floor je AKTIVNÍ v prod (ne za flagem) — korektnostní fix; start penalta default-off do kalibrace. Postaveno paralelním worktree agentem, integrováno + zvalidováno sériově.
## 2026-06-14 — EV: tolerance „dost dobré" — konec honění posledních % do 100 %
- **Problém:** po live-SoC fixu zůstalo malé deadline dobití (~1.33 kWh v 05:00) honící posledních ~2 % k targetu 100 %. live_soc clampnuté na 99 % vs target 100 % → needed_wh nikdy neklesne na 0 → **věčné mini-dobíjení = start/stop nabíječky, Tesla notifikace, zbytečné Modbus zápisy** (cyklování).
- **Mechanismus (fix):** effective target zastropovaný na 99 (= clamp live_soc); `energy_needed_wh = 0` když `live_soc >= least(target,99) tolerance`. Tolerance per-vozidlo: nový sloupec `asset_vehicle.charge_done_tolerance_pct` (default 3 p.b., V107). 0 = tvrdě na target. Ponecháno: anti-fragmentace + 3f `min_power_w` floor (scattered 1f trickle) jako další solver fix (plán bod #3).
- **Soubory:** `V107__ev_charge_done_tolerance.sql`, `R__038_fn_ev_session_planning_json.sql`, `docs/04-modules/ev-charging.md`.
- **Ověření (živá DB):** session #6 home-01 (live_soc 97.9, target 100): `energy_needed_wh` 1329 → **0** (97.9 ≥ 993 = 96). Golden gate: R__038 je upstream solveru (frozen JSON fixtures) → netýká se ho.
## 2026-06-14 — phantom 11 kW okna: plánovač slepý k pokroku nabíjení EV (živé SoC)
- **Problém:** Tesla připojená na 70 %, dotankovaná na ~98 %, ale plán emitoval **15 oken po 11 kW** (20:1523:45) — phantom. `fn_ev_session_planning_json` vracela `energy_needed_wh = 18750 Wh` konstantně po celou session.
- **Příčina:** needed_wh = (target soc_at_connect)/100 × cap `energy_delivered_wh`, JENŽE `energy_delivered_wh` se během session **NIKDY nezapisuje** (V006 DEFAULT 0, žádný updater) → needed_wh konstantní, plánovač slepý k pokroku nabíjení; headroom navíc ze zamrzlého soc_at_connect. **Counter `energy_kwh` (Telto reg 39) je ROZBITÝ** — ověřeno živě: 17.4 kWh reálně nabito, counter stál na 0.18 kWh → coulomb z něj nejde.
- **Mechanismus (fix):** nový `ems.fn_ev_session_delivered_wh(charger_id, since)` = time-weighted integrál **`power_w`** z telemetry_ev_charger (dt cap 120 s; power_w je spolehlivý). R__038 počítá `live_soc = soc_at_connect + delivered/cap`, clamp 99 %; needed_wh i headroom z živého SoC místo zamrzlého soc_at_connect. Fallback `coalesce(live, energy_delivered_wh, 0)` drží staré chování bez telemetrie. Žádné buzení Tesly, funguje i pro Zoe (power-based, bez API).
- **Soubory:** `db/routines/R__038_fn_ev_session_planning_json.sql` (helper fn + přepočet), `docs/04-modules/ev-charging.md`, `docs/ev-improvement-plan-2026-06-14.md`.
- **Ověření (živá DB, read-only psql):** session #6 home-01 — integrál power_w = 17.42 kWh → live_soc 97.9 % (sedí na realitu i na „99 %" z displeje); nová fn `energy_needed_wh` 18750 → **1329 Wh**, headroom 0. Golden gate testuje Python solver downstream R__038 (frozen JSON fixtures), takže SQL změna se ho netýká; fallback drží případné re-extrakce identické.
- **Zbývá (backlog, plán bod 26):** předehřev/0 A (nepouštět 0 A při SoC≥target), anti-fragmentace v solveru (block-start penalta), geofence arrival, proaktivní notifikace, aktivace usage forecast. **Counter reg 39** rozbitý = i audit/ekonomika EV jede naslepo — zvážit fix čtení nebo přepnout audit na integrál power_w.
## 2026-06-14 — HOTFIX: plánovač oslepl k autu po přejmenování wallboxu (hardcoded kódy)
- **Problém:** uživatel přejmenoval wallboxy `ev-charger-1/2``vt-ev-charger-1/2`. fn_planning_site_context (R__039) a fn_load_planning_slots_full (R__063) měly kódy NATVRDO → ctx.vehicles=[], ev_sessions=[null,null], ev1/ev2_connected vždy false → plánovač auto NEVIDĚL → žádné nabíjení ani v záporných cenách (Tesla 70%, okno 0.32 Kč nevyužito).
- **Mechanismus (fix):** výběr wallboxu DYNAMICKY podle site_id, ev1=nejnižší ch.id, ev2=druhý (stabilní, odolné přejmenování). Inverter pro gen_cutoff přes `controllable=true` místo `code='deye-main'`. Konzistentní R__039 (vehicles order by id, sessions dynamické kódy) + R__063 (ev1/ev2 connected).
- **Soubory:** R__039, R__063 (pure SQL). **Ověření:** po deployi ctx vehicles=2, ev_sessions=[True,False], plán nabíjí 14.6 kWh v záporném okně 14:1516:00. 363 testů zelených.
- **Zbývá (backlog):** outputs.py `_current_limit_for_charger` (endswith '-1'/'-2' fallback — funguje, ale křehké u kódů bez suffixu), frontend Settings.tsx hardcoded kódy, notifikace mismatch/clock = asset_code bez site. Doporučení: oddělit `code` (identifikátor) od `name` (zobrazení).
## 2026-06-13 — exekuce: baterie se nedobila v záporných cenách (guard carve-out)
- **Problém:** buy záporný 13:0015:45 (0.47…0.95 Kč), plán ordinoval CHARGE +17 kW import, SoC cíl ~96 %, ale realita SoC 71 % (nabíjení jen z PV, grid≈0). Večer se dokupovalo ze sítě místo z plné baterie.
- **Příčina:** `_apply_export_plan_guard` / `_build_setpoints` (setpoints.py) — false positive: `sell<0` & `grid_sp>=0` splete IMPORT na nabití s exportní situací a překlopí CHARGE→PASSIVE / nastaví export_ban. V PASSIVE Deye nenabíjí ze sítě.
- **Mechanismus (fix):** carve-out — když slot je CHARGE (`pm=='CHARGE'`) nebo importuje na nabití (`grid_sp>0 & bat>0`), guard vrátí sp beze změny a export_ban se nenastaví. §6 zakazuje jen export, ne import (§7).
- **Soubory:** backend/services/control/setpoints.py, test_control_export_plan_guard.py (test_neg_sell_grid_charge_not_blocked), docs/audits/planner-neg-buy-charge-not-executed-2026-06-13.md.
- **Ověření:** guard testy 47 passed; živě — záporný den → grid_w roste, SoC k cíli. Mimo solver → golden gate / solver_v2_eval beze změny.
## 2026-06-13 — EV session viditelná i bez deadline; reg 15 re-asert (2 bugy home-01)
- **BUG1 (Modbus zápis EV rozbitý):** od ~22:45 UTC 12.6. nevznikl žádný telto journal řádek (ani failed), auto jelo failsafe 8 A místo plánovaných 0 A. **Příčina:** reg 15 (amps) byl write-on-change proti journalu (`fn_modbus_device_state_map`). Jakmile měl reg 15 řádek „0 verified", a plán dál chtěl 0, **nikdy nevznikl nový příkaz** — a TeltoCharge si po výpadku komunikace sám přepsal reg 15 na failsafe (reg 20) **bez journal řádku**. Verify čte zpět jen `written` řádky, takže drift 0 → 8 A nikdo neviděl ani neopravil (tichá divergence). **Fix:** reg 15 se zapisuje **každý tick** (re-asert), reg 19/20 zůstávají write-on-change (EEPROM); per-charger failsafe/timeout (V106 `asset_ev_charger.watchdog_failsafe_a` / `watchdog_comm_timeout_s`). „Zákaz nabíjení" = reg 15 = 0 (protokol rev 0.5 nemá samostatný enable registr).
- **BUG2 (plánovač slepý k autu):** aktivní plán měl `ev_sessions:0`, ač session běžela (target 70 %) → plán neviděl ~6 kW zátěž, špatně rozvrhl baterii (zbytečný večerní import). **Příčina:** `fn_planning_site_context` vracela session jako `null`, když `needed_wh=0` (auto nad targetem) i když `target_deadline is null`; navíc `_ev_session_from_json` zahazovala session bez deadline (Python). **Fix:** R__038 `fn_ev_session_planning_json` — session se vyřadí jen bez tvrdých dat (kapacita / soc_at_connect); `target_deadline` smí být NULL (solver hard constraint aplikuje jen při needed>0; oportunistická vrstva běží i bez deadline). `_ev_session_from_json` si NULL deadline ponechá.
- **Soubory:** V106, R__038, R__039 (volá helper), `services/control/outputs.py`, `services/planning/db_io.py`; testy `test_ev_write_on_change.py`, `test_ev_session_parse.py`; docs teltocharge / journal / ev-charging.
- **Ověření:** `pytest -q` 362 passed; golden replay gate 7 passed; solver_v2_eval beze změny (fixtures bez EV session — golden potvrzuje žádnou regresi na neEV cestě).
- **K ROZHODNUTÍ (nenasazeno):** agresivnější oportunistický algoritmus z cen (P50 levných oken z `market_price_stats` místo konstanty 1 Kč/kWh) — návrh v `docs/04-modules/planning.md` sekce „EV oportunismus — návrh".
## 2026-06-13 — degradační cena dle skutečných cen packů (V103)
- **Problém:** seedy nesly default 0.50 Kč/kWh u KV1/BA81/HU1 — u malých packů zabíjel mělké arbitráže, u HU1 zkresloval studii spotové smlouvy.
- **Mechanismus:** majitel dodal ceny packů (home-01 150 tis./64 kWh, HU1 300 tis./128 kWh, KV1 80 tis./12.5 kWh, BA81 70 tis./12.5 kWh); při ~1 cyklu/den jsou packy calendar-bound → parametr slouží jako šumový floor (ochrana marže), ne plná cena cyklu. V103: home-01+HU1 0.15, KV1+BA81 0.25 (sníženo z 0.50).
- **Ověření:** stropy plné ceny cyklu 0.39 / 0.931.07 Kč/kWh; HU1 studie citlivost deg 0.5 = stále +104 tis. Kč/rok. Hlídat fn_battery_cycle_audit (>2 cykly/den → přehodnotit).
## 2026-06-12 — home-01: ulice z nového externího CT (reg 619), celková spotřeba domu
- **Problém:** Deye hlásil export 13.5 kW, fakturační elektroměr ~8 kW; load 164 W při prokazatelném nabíjení EV 10.5 kW. Audit nadhodnocoval výnos exportu (a podhodnocoval import) po celou historii: hlavní okruhy domu vč. wallboxu visí MEZI střídačem a elektroměrem, takže je střídač nikdy neviděl (reg 625 = svorky, reg 653 = jen UPS port).
- **Kontext:** majitel 2026-06-11/12 doinstaloval externí CT k fakturačnímu elektroměru a přepnul režim — od té chvíle existuje pravdivé měření ulice (reg 619; ověřeno: -8.3 kW přesně dle elektroměru).
- **Mechanismus:** V100 deye_zero_export_mode=2 (zero export podle CT; exporter drží reg 142); collector čte ulici z reg 619 (s CT; bez CT fallback 625), load_power_w = pv + baterie + grid (celkový dům, clamp >= 0); nové sloupce inverter_grid_port_w (625) a ups_load_w (653).
- **Soubory:** V100__home01_grid_ct.sql, R__049, R__052, telemetry_collector.py, modbus-registers.md.
- **Ověření:** reg 619 vs elektroměr (8.3 vs ~8 kW); bilance pv+batt-CT = 5.5 kW odpovídá běžícím spotřebičům.
- **Pozn.:** historická grid_power_w (do 2026-06-12) = svorky střídače; ekonomiku auditu srovnat s fakturou. Baseline rebuild po ~týdnu nových dat: fn_rebuild_consumption_baseline_stats (starý load = jen UPS port).
## 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
400900 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.32.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, 619 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.13.2 Kč (vs jaro 45.2), neg dny ~0 → klíč je TČ track. v2 bez
sezónních oken (v1 měl 17h/511h/AM-PM = jarní slunce).
---
## 2026-06-11 — Refaktor „Čistý plánovač“: harness, dekompozice, solver_v2 (Fáze 03)
**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,410 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 1722h 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:4508: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,81 kW). Reg **145** (`export_ban`) nestačí — mikroinvertory na GEN portu exportují, dokud reg **178** bits 01 ≠ 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 45 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:0006: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:0022: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 **~2430 %** 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 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í:**
```