Files
ems/docs/planning-changelog.md
Dusan Vojacek 7036bcfdb8
Some checks failed
CI and deploy / migration-check (push) Failing after 14s
CI and deploy / deploy (push) Has been skipped
a dalsi fix
2026-05-25 00:47:06 +02:00

403 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Planning / LP — changelog
Změny v plánovači (`planning_engine.py`, `R__063_fn_load_planning_slots_full.sql`) a souvisejících testech.
Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověření.
---
## 2026-05-27 (e) — pre-`buy<0` discharge_export window + SoC cap (v15)
**Problém (home-01 run 16636, tag v14):** SoC cap 47 % spočítán správně (`pre_neg_buy_soc_cap_wh=29800`), ale LP **necilí** — 12:45 SoC = 98,4 %, slack 33 kWh × penalty 5 Kč/kWh = 165 Kč LP přijímá. Důvod: startovní SoC 58 % > cap 47 %, a R__063 v noci nepovoluje `allow_discharge_export=true`**LP nemá cestu jak baterii vybít** kromě malé self-consumption. Tvrdý `bc_pv[t] = 0` mimo charge_slots by vyřešil PV pumping, ale rozbije bilanci v testech kde `pv_surplus > 0 + sell < pv_store_val` (export blokován v Pythonu).
**Oprava (tag `2026-05-27-hard-pv-mask-v15`):**
- **Cap snížen na `min_soc + 0.15 × usable`** (~25 % SoC) místo dynamického z `neg_window_capacity`.
- **Penalty zvýšena 10×** na 0,05 Kč/Wh (= 50 Kč/kWh), nad marginal arbitrage (~5 Kč/kWh).
- **Když start SoC > cap_raw**, rozšíříme `discharge_export_slots` o sloty před `first_neg_buy_idx` se `sell ≥ 0.8 × max(sell_pre_neg_buy)`. LP tak může vybít baterii **přes ge_bat** v noci/ráno za sell ~3 Kč/kWh; pak v buy<0 okně nasáje z OTE záporných cen.
- Když start ≤ cap_raw, cap = current_soc (constraint „nepřibývat" místo „klesnout" → triviálně splněno, LP nikdy nedostane infeasible kvůli rezervaci).
**Ekonomická logika home-01 zítra:**
- Vybít 20 kWh v noci za sell~3 Kč/kWh = ~60 Kč
- Nabít 20 kWh v buy<0 okně za buy~0.22 Kč/kWh = ~+4 Kč (acquisition záporný)
- Vyprodat v peak za sell~4.4 Kč/kWh = ~88 Kč
- Total alternative: ~152 Kč (vs current bez vybíjení ~132 Kč)
**Ověření:** v `solver_params.inputs` nově: `first_neg_buy_idx`, `pre_neg_buy_soc_cap_wh`, `pre_neg_buy_soc_slack_wh`. Replan home-01 → SoC v 12:45 ≤ 25 % (nebo cap = start SoC, pokud bottleneck export). `bat_setpoint_w < 0` v některých nočních / ranních slotech s peak sell.
---
## 2026-05-27 (d) — pre-`buy<0` SoC cap v LP (v14)
**Problém (home-01 run 16622, tag v13):** Fix 1 (acquisition ≥ 0) i Fix 2 (R__063 `v_pv_layer_cap_wh` redukce) byly nasazené, ale plán pro 2026-05-25 zůstal stejný: 10:30 SoC = 95 %, 11:00 SoC = 98,3 %, 13:0014:45 (buy<0, sell<0) baterka plná → export pole A do mínusu + curtail 5 kW pole A. Příčina:
- LP v `solve_dispatch`**bc_pv[t] ≤ pv_surplus_w** i pro sloty `t not in charge_slots` (Python obchází tvrdou masku R__063 pro PV charging) → R__063 Fix 2 (vrstva A cap = 0) nemá efekt.
- `acquisition` je v LP **vstupní konstanta**, ne endogenní funkce slotů → LP nevidí per-slot opportunity cost (nabíjení v `buy<0` = záporná cena vs PV = 0) → LP rovnoměrně nabíjí kdekoliv má PV.
- Marginal arbitrage = peak_sell (4,40) avg_neg_buy (0,22) = **4,62 Kč/kWh**, vs PV→bat (acquisition 0,757) = 3,64 Kč/kWh — rozdíl ~1 Kč/kWh × 32 kWh denně.
**Oprava (tag `2026-05-27-pre-neg-buy-soc-cap-v14`):** Tvrdý LP constraint na SoC v posledním slotu před prvním `buy<0`:
- `neg_buy_indices = [t for t,s in slots if buy<0]`
- `neg_window_capacity_wh = Σ min(max_charge_w, pv_surplus + grid_max_import) × 0.25 × eff` přes neg_buy sloty.
- `pre_neg_buy_soc_cap_wh = max(min_soc_wh, soc_max_wh min(neg_window, 0.9 × usable))`.
- LP: `soc[first_neg_buy_idx 1] ≤ pre_neg_buy_soc_cap_wh + slack`, slack penalizován 0,005 Kč/Wh (= 5 Kč/kWh, lehce nad marginal arb → LP cap dodrží, ale neinfeasible při krátkém horizontu / vysokém startovním SoC).
**Důsledek:** LP musí baterii do `buy<0` okna dorazit s volnou kapacitou — místo ranního PV nabíjení (sell≥0 sloty) export pole B (green bonus 7,135 Kč/kWh) a curtail pole A; v `buy<0` okně max import + max PV → baterka plná; večer max export.
**Ověření:** v `solver_params.inputs` nově: `first_neg_buy_idx`, `pre_neg_buy_soc_cap_wh`, `pre_neg_buy_soc_slack_wh`. Replan home-01 zítra (2026-05-25) → SoC v 12:45 ≤ cap (cca 1015 %), 13:0014:45 SoC stoupá z capu k 100 %, `pv_a_curtailed_w` v okně blíží 0.
---
## 2026-05-27 (c) — rezervace SoC pro `sell<0` okno + fallback acquisition ≥ 0 (v13)
**Problém (home-01 run 16614, tag v12):** Aktivní plán pro 2026-05-25:
- 10:30 SoC = 96,9 %, 10:45 SoC = 98,3 % (baterie plná z PV ráno) → odpoledne v `sell<0` slotech (13: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í:**
```