Files
ems/docs/planning-changelog.md
Dusan Vojacek 8494ea26de
Some checks failed
CI and deploy / migration-check (push) Failing after 28s
CI and deploy / deploy (push) Has been skipped
nerezta PV A pri prodeji z baterie
2026-05-26 07:34:52 +02:00

42 KiB
Raw Blame History

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-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 < 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.

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 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<0, PV A curtail jen v buy<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<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<0 vs sell<0 (v22 / v22b)

v22b — Infeasible: Tvrdý is_daytime_pv_surplus + ge_pv=0 z pv_store blokoval export před buy<0. Oprava: jen měkká PRE_NEG_CHARGE_PENALTY; u buy&lt;0 přeskočit sell<0 ventil; neg_buy shortfall jen na posledním buy<0 slotu; retry relaxed_neg_buy_charge. Tag 2026-05-28-buy-sell-split-v22b.


2026-05-28 — rozlišení buy<0 vs sell<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<0 (05:3007:00, buy≥0): nabíjení z PV/sítě místo přípravy kapacity.
  • Při buy<0 (12:1512:45): export do sítě místo importu — ventil w_pv_b_vent u sell<0 platil i když buy<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<0 a buy≥0: export pole B / curtail A (v21b), bez neg_sell_soc shortfallu v buy<0 slotech.
  • buy<0: tvrdě ge=ge_pv=ge_bat=0 + měkký neg_buy_charge_shortfall (tlak na bc_gi+bc_pv).
  • sell<0 + buy<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<0.


2026-05-28 — ranní sell<0: držet SoC před buy<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<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<0 v sell<0.

v21b — Infeasible: bc_pv=0 + ventil w_pv_b_vent (export jen při plné baterii) → přebytek pole B (pv_b > load) nemá kam (bilance). Oprava: před first_neg_buy_idx povolit ge_pv ≤ pv_b bez ventilu; safety soc_max u sell<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<0, neg-buy shortfall). Zůstává stabilní základ:

  • v17: bc_gi=0 při sell<0+PV+buy≥0; ge_pv ≤ pv_b při sell<0
  • v18: večerní export push z DB min(discharge, export) W

Strategie před buy<0 / import v buy<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<0 slotu + sell<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_exportge_bat=0 (sell<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<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<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_bundleeconomics_summary.

Ověření: pytest backend/tests/test_planning_economics_columns.py, DynamicGridFilterTests, Home01RegressionTests::test_home01_no_night_charge_before_pv_day, test_two_pass_converged_after_filter; po deploy MCP: grid_charge_suppressed_reason ve fn_load_planning_slots_full, two_pass_converged=true na novém run.


2026-05-26 (o) — home-01: neg. výkup bez placeného exportu FVE + dump baterie před extrémním buy

Problém (run 16480, tag v10): Po ranním nabití na soc_max solver při sell<0 exportoval celý PV přebytek (~9 kW, PV_SURPLUS) — binárka w_pv_full_neg povolila ge_pv ≤ pv_surplus místo jen ventilu pole B. Zároveň ge_bat=0 blokoval výboj baterie před oknem buy ≤ 2 (round-trip arbitráž).

Oprava (tag 2026-05-26-neg-sell-bat-dump-extreme-buy-v11):

  • Spot sell<0: ge_pv=0 dokud není plná baterie; při plné jen ge_pv ≤ pv_b (w_pv_b_vent_neg) + penalizace NEG_SELL_PV_B_VENT_PENALTY (4 Kč/kWh).
  • Před extrémním buy (buy ≤ planner_extreme_buy_threshold, default 2): v okně 12 slotů smí ge_bat>0 při sell<0, pokud min_buy_future < sell degrad.
  • Odstraněn w_pv_full_neg (export celého surplusu).

Ověření: test_neg_sell_full_battery_exports_at_most_pv_b_not_full_surplus, test_neg_sell_bat_dump_before_extreme_buy, test_neg_sell_pv_to_battery_not_grid_when_soc_has_room; po deploy replan home-01 — neg sell bez ~9 kW exportu.


2026-05-25 (n) — home-01 AUTO: záporný výkup bez exportu, večerní špička

Problém (run 16412, AUTO): Dnes večer téměř bez exportu (terminal SoC drží energii na zítřek); zítra 07:30+ masivní PV_SURPLUS při sell<0 místo nabíjení; zítra večer export OK.

Příčiny:

  1. Spot při sell<0: skip_pv_store_block kvůli pv_b povoloval export i s prázdnou baterií.
  2. R__063 večerní maska spot: sell > ref_buy — ve slotu často sell < buy, večerní export dnes vypnutý.
  3. Večerní ge_bat push jen 50 % výkonu vs. terminal SoC shadow.

Oprava (tag 2026-05-25-home01-neg-sell-evening-v10):

  • Spot sell<0: ge_pv/ge jen pokud soc[t-1] ≥ soc_max headroom (binárka w_pv_full_neg).
  • R__063: večerní peak u spotu jako u fixního tarifu (denní max výkupu).
  • PEAK_EXPORT_SHORTFALL 80 Kč/kWh; večerní push na plný EVENING_BATTERY_EXPORT_MIN_W.

Ověření: Home01RegressionTests::test_neg_sell_pv_to_battery_not_grid_when_soc_has_room; po deploy replan home-01 — neg sell bez exportu při SoC < max.


2026-05-25 (m) — BA81: záporný výkup bez exportu podle DB purchase_pricing_mode

Problém (tag v8 v produkci): KV1 OK; BA81 pořád export při sell < 0 (dnes i zítra). v8 používalo _horizon_fixed_tariff_like (rozptyl buy < 0,25 Kč/kWh). U BA81 buy skáče NT/VT (3,09 ↔ 4,09) → heuristika false → zákaz exportu se neaplikoval.

Oprava (tag 2026-05-25-purchase-fixed-neg-sell-v9):

  • ems.fn_planning_site_context vrací market.purchase_pricing_mode / sale_pricing_mode z site_market_config.
  • Při sell < 0 a purchase_pricing_mode = fixed: ge = 0 (nezávisle na rozptylu buy). home-01 (spot nákup) výjimku nemá — může ventovat PV B.
  • _horizon_fixed_tariff_like zůstává jen pro drahý import / charge_acquisition (heuristika + DB fixed).

Ověření: pytest …::NegativeSellPvChargeTests::test_ba81_fixed_purchase_nt_vt_buy_spread_neg_sell_no_export; po deploy + replan BA81: žádný grid < 0 při sell < 0 v MCP.


2026-05-25 (l) — Plán 25. 5.: BA81 neg. výkup bez exportu, KV1 ranní curtail

Problém (MCP plán run 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-v6ge == 0 jen při block_export_on_negative_sell; ge_pv ≤ pv_b × (1 z_gen_cutoff); v5 večerní push + pv_b cap zůstávají.

Ověření: pytest backend/tests/test_planning_dispatch_milp.py::NegativeSellPvChargeTests; MCP po deployi: planner_build_tag = 2026-05-24-ba81-gen-cutoff-v6.


2026-05-24 — Arbitráž: OTE místo hodin, export ve špičkách, FVE při sell<0

Problém: Plán ukazoval slabé nabíjení/vybíjení (KV1, BA81) přestože ekonomika (OTE) favorizovala opak. Ve špičkách MILP nevybíjel baterii naplno; noc BA81 držela SoC na rezervě bez exportu; záporný výkup neplnil FVE do baterie.

Změny:

Oblast Co Proč
R__063 — exportní maska Místo pevného vyloučení 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=0ge_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í):

# 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:

-- 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 15810max_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:

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 15826pv≈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:

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

## YYYY-MM-DD — Krátký titul

**Problém:****Změny:****Soubory:****Ověření:**