76 KiB
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-01 — KV1: noc z baterie, ne import za 6,35 Kč (v62)
Problém: Po večerním vývozu (~32 % SoC) plán 22:00–06:00 krmil dům ze sítě (grid ~260 W, bat 0) místo z baterie. Fixní buy ≈ charge_acquisition ≈ 6,35 → expensive_import_slot nikdy true → neplatilo bd ≥ load ani noční penalizace importu (buy > acq je false).
Změna (v62): u purchase_pricing_mode=fixed: expensive_import_slot = true (buy ≥ 0); _night_self_consume_discourage_import_indices zahrne noční sloty i při buy = acq.
Tag 2026-06-01-kv1-fixed-night-self-consume-v62.
2026-06-01 — home-01: grid jen při buy ≤ acquisition (v61, zrušeno v60)
Problém: 19:00 nabíjení za buy ~5,5 při charge_acquisition ~3,25 z rána → falešně ziskový večerní export. v60 (sell < buy ve slotu) bylo špatně: u spotu (a často u fixního tarifu) je sell < buy normální (marže distributora) — arbitráž je mezi sloty, ne v jedné čtvrthodině.
Změna (v61): spot: bc_gi = 0 jen když buy[t] > charge_acquisition + degrad (nákup v drahém slotu, ne levný NT). Export/push: zpět sell > acq + degrad. Fixní: bc_gi dál jen sell > min_sell_horizon (v59 SQL min sell); bez pravidla sell < buy.
Tag 2026-06-01-spot-grid-charge-at-acq-buy-v61.
Ověření: pytest … -k spot_no_grid_charge_when_buy_above_acquisition.
2026-06-01 — BA81/KV1: zákaz grid nabíjení mimo min sell + večer bez charge (v59)
Problém (po v58): KV1 po večerním vývozu 22:00–22:15 nabíjel ze sítě za buy ~6,35 Kč (allow_charge z R__063 podle slot_ord, ne nejnižší sell). BA81 v špičce sell ~9,6 částečně nabíjela (allow_charge + PV), místo čistého vývozu; 03:30 grid nabíjení před východem slunce.
Příčina v58: bc_gi = 0 jen při pv_surplus > 500 W — v noci prázdné, grid nabíjení projde.
Změna (v59):
- LP:
fixed_grid_charge_unprofitable—bc_gi = 0kdyžsell < buy + degradnebosell > min_sell + 0,20(bez podmínky na FVE). - LP: v
evening_push_tspřisell > buy→bc_pv = bc_gi = 0(jen vývoz). R__063: u fixního tarifu grid sloty jen kdesell ≤ min(sell≥0) + degrad + 0,05, řazenísell_price ASC(neslot_ord).
Tag 2026-06-01-fixed-grid-charge-min-sell-v59.
Ověření: pytest … -k "fixed_no_grid_charge or fixed_evening_push_no_charge"; po deployi Flyway + backend replan KV1/BA81.
2026-06-01 — BA81/KV1: FVE export při vysokém sell, nabíjení u min sell (v58)
Problém: U purchase_pricing_mode=fixed (BA81 buy ~3,09 Kč, KV1 ~6,35 Kč) oproti home-01 (spot):
- půlnoc→ráno baterie ~24–30 % bez vývozu i tam, kde
sell > buy + degrad(BA81 úsvit ~3,35 Kč); - ~06:00 nabíjení z FVE při výkupní ~3 Kč/kWh, místo exportu přebytku a nabíjení až u nejnižšího sell v horizontu (~1,5 Kč, poledne);
- KV1 večer jen malý vývoz ve špičce, ráno zbytečně vysoká rezerva.
Příčiny v LP: chybějící bc_pv == 0 při sell výrazně nad denním minimem; pv_store / ge_pv == 0 u fixního tarifu mimo úzké fixed_pre_neg_*; evening_early_export_ban (ge_bat = 0) i pro profitable sell > buy v noci; peak_export_shortfall noční okno přeskakoval (pravidlo pro spot / evening_push).
Změna (v58) — backend/services/planning_engine.py:
- Konstanta
FIXED_PV_CHARGE_ONLY_NEAR_MIN_SELL_CZK_KWH = 0.20;fixed_horizon_min_sell_pre=min(sell_price)prosell ≥ 0v horizontu. fixed_high_sell_no_pv_charge:sell > min_sell + 0,20a PV přebytek >NIGHT_EXPORT_PV_SUNRISE_SURPLUS_W→bc_pv = 0,bc_gi = 0; rozšířenskip_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í:
cd backend && pytest tests/test_planning_dispatch_milp.py \
-k "fixed_high_sell_no_pv_charge or fixed_night_profitable" -q
MCP po replanu BA81/KV1: planning_run.solver_params->>'planner_build_tag' obsahuje v58; ráno export (grid_setpoint_w < 0, battery_setpoint_w malé), poledne nabíjení (battery_setpoint_w vysoké u min sell).
2026-06-01 — home-01: večerní vývoz po relaxed_expensive_import (v57)
Problém: v55 při jakékoli relaxed větvi vynulovalo evening_push_ts → evening_early_export_ban zakázal ge_bat i při sell ~9,6 Kč/kWh; baterie jen samospotřeba, zítra export FVE za ~2 Kč.
Změna (v57): evening_push_ts se nemazá při relaxed_expensive_import / relaxed_neg_buy_charge; tvrdý push jen při relaxed_neg_prep_window / neg_sell_phases_fallback (evening_push_hard_suppressed). Fallback: alespoň jeden večerní peak slot. Snap: evening_push_hard_suppressed, evening_push_peak_fallback_used.
Tag 2026-06-01-evening-push-keep-on-relaxed-import-v57.
2026-05-31 — home-01: ranní tvrdý export + pass2 (v56)
Problém: Po v55 stále 422 Solver: Infeasible u ručního replanu. Příčina: tvrdé ge_bat push v morning_pre_neg_export_ts zůstávalo aktivní i při relaxed_* (25 % SoC + neg den 31.5. → nelze exportovat ráno a zároveň splnit prep). Pass2 two-pass mohl spadnout i když pass1 prošel.
Změna (v56): tvrdý ranní/pre-neg export jen bez any_relaxed; pass2 při Infeasible vrátí pass1. Snap: morning_pre_neg_export_hard, any_relaxed_solve, two_pass_pass2_infeasible_used_pass1.
Tag 2026-05-31-morning-export-relaxed-v56.
2026-05-31 — home-01: evening push při každém relaxed retry (v55)
Problém: v54 maže tvrdý push až u relaxed_neg_prep_window (3. retry). Retry 1–2 pořád držely vypočtený evening_push_ts → u ~25 % SoC často stále Infeasible. Ruční „Přeplánovat“ navíc spadlo, když v2 comparison peer selhal (active v1 prošel). V DB po pádu žádný api run — scheduler v51/v54 mezitím OK.
Změna (v55): tvrdý evening_push_ts = ∅ při jakékoli relaxed vlajce; rolling commitment ignorovat od relaxed_neg_buy_charge; comparison peer = solve_dispatch_two_pass + non-fatal skip. Snap: evening_push_cleared_on_relaxed_prep, charge_commitment_ignored_on_relaxed.
Tag 2026-05-31-evening-push-any-relaxed-v55.
2026-05-31 — home-01: tvrdý evening push po relaxed prep (v54)
Problém: v53 maže jen hysterézní override, ne vypočtený evening_push_ts. Po relaxed_neg_prep_window (typicky home-01 ~25 % SoC + neg den 31.5.) zůstávaly tvrdé ge_bat/z_export v push slotech → Solver: Infeasible i po celém retry řetězci. Pass2 two-pass znovu aplikoval override bez carryover relaxace.
Změna (v54): při relaxed_neg_prep_window → evening_push_ts = ∅; _solve_dispatch_relax_carryover — pass2 dědí nouzové vlajky z pass1, evening_push_ts_override=None. Snap: evening_push_cleared_on_relaxed_prep.
Tag 2026-05-31-evening-push-relaxed-clear-v54.
Ověření MCP (home-01): planner_build_tag = v54; po ručním replanu relaxed_neg_prep_window: true, evening_push_ts: [], run status = active.
2026-05-31 — home-01: Infeasible při rolling hysteréze push (v53)
Problém: Po v52 KV1 OK, home-01 občas Solver: Infeasible — rolling replan držel evening_push_ts z minulého běhu (hystereze) i v retry větvích; tvrdý ge_bat push při nízkém SoC / změně slotů.
Změna (v53): _evening_push_override_for_solve — override vypnout při jakémkoli relaxed retry; _filter_evening_push_override_indices — jen sloty s allow_discharge_export, bez defer PV, s dosažitelným push floorem. Snap: evening_push_override_dropped_on_retry.
Tag 2026-05-31-evening-push-override-retry-v53.
Ověření: pytest … -k stale_evening_push_override; rolling home-01 bez RuntimeError.
2026-05-31 — KV1: večerní push vs ranní max sell (v52)
Problém: KV1 večer ~3,3 Kč neprodával do sítě (evening_push prázdný: sell < acq+spread ≈ 6,65), vývoz až úsvit ~2,8 Kč před sell<0 (08:15). Příčina: pravidla v41 evening_early + v47 push profitabilita z home-01 na fixní acquisition.
Změna (v52): _kv1_block_export_fixed_evening_push — u fixed + block_export_on_negative_sell večerní push kandidát když sell ≥ max(sell 5–11 před 1. sell<0) − degrad (ne sell > 6,35+spread). Bez neg dne v horizontu: sell ≥ 1 Kč. Snap: kv1_evening_push_morning_peak_rule.
Tag 2026-05-31-kv1-evening-push-morning-peak-v52.
Ověření: pytest … -k kv1_evening_push_when_sell_above_morning; MCP KV1 večer BATTERY_SELL, evening_push_ts neprázdný.
2026-05-31 — BA81 úsvit: žádný plný curtail A / zápis reg 340 (v51)
Problém: Při malém ranním PV (např. 405 W A, 49 W B) LP kvůli fixed_pv_b_export_cap (ge_pv ≤ pv_b) usekl celé pole A (curt_a = pv_a) a exporter posílal reg 340 z nepřesného forecastu — zbytečný HW zápis, baterie prázdná.
Změna (v51):
fixed_pv_b_export_capjen kdyžpv_a_forecast ≥ 1500 W(DAWN_LOW_PV_NO_CURTAIL_W).fixed_mi_low_pv_surplus_export: úsvit + MI + přebytek → neblokovatge_pvpřes pv_store.setpoints.py: přiforecast < 1500 Wacurt_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>0) plán BATTERY_SELL místo PV_SURPLUS (home-01 OK). Příčiny: skip_pv_store_block jen před 1. sell<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 → neblokovatge_pv(pv_store).battery_export_defer_pv_ts:ge_bat=0,z_export=0(výjimky: morning pre-neg / pre-neg buy větve).evening_push_ts: přeskočit push, když platí defer.
Tag 2026-05-31-kv1-pv-surplus-over-bat-export-v50.
Ověření: pytest … -k kv1_evening_battery_push; MCP KV1 17:00–18:00: export_mode=PV_SURPLUS, curt_a malé.
2026-05-31 — Večerní push: celý Wh rozpočet jen pro dnešní noc (v49)
Problém (v43): push_budget / počet_kalendářních_večerů dělil aktuální SoC mezi dnešní a zítřejší večer v horizontu — přes den FVE / neg nabíjení. Dnes večer dostal ~polovinu rozpočtu → chyběly sloty (např. 23:15); zítra večer push z dnešní SoC nedává smysl.
Změna (v49):
_primary_night_export_segment_indices— první noční epizoda (17h → východ FVE) od začátku horizontu._evening_push_soc_budget_calendar_segments— push Wh jen pro kalendářní večer v této epizodě; jeden společný rozpočet, kandidáti sell desc přes zbývající sloty.- Hysteréze (
_rolling_evening_push_override): drží jen sloty z budget-eligible množiny.
Tag 2026-05-31-evening-push-budget-primary-night-v49. Zítřejší večer → vlastní rolling replan po dni.
Ověření: pytest … -k evening_push_budget_only_primary; MCP: planner_build_tag v49, evening_push_ts bez zítřejších 18:30+ při replanu dnes večer; více dnešních push slotů při stejné SoC.
2026-05-31 — Podlaha vývoje reserve 20 %, žádný curtail slabé FVE za úsvitu (v48)
Problém (běh 20728, v47): Večer + 03:00–03:15 ranní peak export → SoC ~13,5 % (pod reserve 20 %). 05:15–06:00 Prague (= 03:15–03:45 UTC) plán řeže celou PV A (curt_a = pv_a při ~86–346 W) — ge_pv=0 kvůli sell < future_sell (večerní peak v horizontu).
Změna (v48):
- Rozpočet push + podlaha SoC:
reserve_soc_wh, nemin_soc_wh(10 %). - Ranní peak export:
soc[t] ≥ reservev peak slotu. DAWN_LOW_PV_NO_CURTAIL_W: přisell≥0apv_a < 1500 Wneblokovatge_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<buy) je správně — vyprázdnění před neg dnem/FVE. Špatně je po pushu držet SoC a kupovat dům za ~5 Kč.
Problém (v45–v46): Po pushu SoC ~36 %, pak 22:00+ grid import pro baseload; relaxed_expensive_import obešel bd≥load.
Změna (v47):
- Večerní push: zůstává sell > acq+spread (v46 sell≥buy zrušeno).
post_evening_push_night_ts: po posledním push slotu večera → tvrdé bd krmí dům i přirelaxed_expensive_import.night_self_consume+ v45 neg okno beze změny.
Tag 2026-05-30-post-push-night-battery-v47. (v46 na serveru nepoužívat — blokoval večerní push.)
2026-05-29 — Neg okno: grid nabíjení + noc z baterie (v45)
Problém (v44 běh 20282): (1) Po večerním pushu 22:00+ import ze sítě ~3,3 kW při SoC 56 % — night_self_consume jen na podmnožině evening_early_export_ban, ne celá noc. (2) 07:45–08:15 sell<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 mimoevening_push(buy > acq+spread).- R__063
neg_window_grid_charge: od 1. sell<0 na neg denallow_charge+allow_grid_chargepro sell<0 a buy≥0 i bez FVE přebytku. - LP: při
relaxed_neg_prep_windowbezprep_soc_shortfallpenalizace (žá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<0 (v44)
Problém (v43 na home-01 30. 5.): Ráno 05:45–07:30 grid+bat nabíjení za ~2,6–3,7 Kč/kWh → SoC ~99 % ještě před 07:45 sell<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_unlockjen na dnech bez sell<0, hodiny 11–16 (normální odpolední→večerní arbitráž).neg_day_no_grid_before_neg_sell: na neg kalendářní denallow_grid_charge=falsepro všechny sloty před 1. sell<0._neg_sell_pv_forecast_charge_wh: zpětná projekce soc_need z FVE A+B surplusu, ne jen B.- LP:
bc_gi[t]=0před 1. sell<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 < ~90 %; po svítání PV A ne plný curtail.
2026-05-29 — Noc: vlastní spotřeba + večerní arbitráž + push per den (v43)
Problém: (1) Po v42 push exportu plán přes noc držel SoC ~60 % a krmil dům ze sítě za ~5 Kč/kWh místo baterie (acq ~0,7 Kč). (2) Tvrdý push zahrnoval 02–06h (sell < 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: mimoevening_pushpenalizace importu pro dům (gi × surcharge), LP preferujebdpro load.- Push jen ≥17h Prague (
_in_evening_push_hour_window); ne predawn 02–06h. - Push rozpočet per kalendářní večer (
_evening_push_calendar_segments), ne globální greedy přes celou noc. - Push kandidáti jen
allow_discharge_export(SQL maska). - R__063
evening_arbitrage_unlock: před prvním sell<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=0mimo push) a vypnutýpeak_export_shortfallv noci beze změny.
Soubory: backend/services/planning_engine.py (_evening_push_segment_candidates, _evening_battery_export_push_indices), backend/tests/test_planning_dispatch_milp.py (test_evening_no_spread_export_below_segment_peak_home01, test_evening_push_respects_wh_budget_not_all_profitable_slots). Tag 2026-05-29-evening-push-budget-rank-v42.
Ověření: pytest … -k evening_no_spread; MCP: solver_params->'inputs'->'evening_push_ts' — délka ≈ floor(budget_wh / per_slot_wh); každý push slot → |grid_setpoint_w| ≈ 12,5–13,5 kW; sloty mimo push → bez exportu.
2026-05-29 — Večerní export jen ve špičkových slotech (v41)
Problém: home-01 večer ~7,5 kW export v mnoha levnějších slotech (~3,2 Kč) místo plného 13,5 kW v max-sell slotu. Tři důvody: (1) evening_push kandidáti = široké pásmo peak−degrad (0,15 Kč); (2) měkká penalizace peak_export_shortfall tlačila ge_bat i v levnějších nočních slotech; (3) push se neaktivoval, když horizont měl konstantní buy → mylně „fixní tarif“ a sell < buy (přitom večerní export dává smysl vůči acq+spread).
Změna (v41):
- Push kandidáti = sloty se
sell = maxv nočním úseku + maržeacq+spread(spot), nebuy+spread. evening_early_export_ban:ge_bat=0ve všech nočních exportních slotech mimoevening_push(výjimky: pre-neg / neg-evening větve).peak_export_shortfallse v nočním okně neaplikuje.
Soubory: backend/services/planning_engine.py (_evening_push_peak_candidates, _evening_early_export_penalty_indices), backend/tests/test_planning_dispatch_milp.py (test_evening_no_spread_export_below_segment_peak_home01). Tag 2026-05-29-evening-peak-only-export-v41.
Ověření: pytest … -k evening_no_spread_export_below_segment_peak_home01; MCP: večerní slot s max sell → |grid_setpoint_w| ≈ 12,5–13,5 kW; sousední levnější sloty → export_mode=NONE, grid_setpoint_w≥0.
2026-05-29 — Infeasible rolling: relax neg-prep okno (v40b)
Problém: Po načtení OTE na 30. 5. (neg sell) rolling/home-01 končil Solver: Infeasible od ~13:15; ruční replan stejně. Plán zůstal na runu z 13:00 (horizont jen do 22:00). Log často prázdný — výjimka se loguje na WARNING, scheduler ji polyká.
Změna (v40b): Třetí retry relaxed_neg_prep_window (bez večerního push/kotvy + prep hold binárek); čtvrtý retry s planner_neg_sell_prep_soc_percent=100 (fáze sell<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<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 (bezsoc_estřetězení)._pre_neg_pv_export_forecast_cushion_ok_for_day: pokudobserved_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 vneg_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 < −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<0 + termika + bazén
Soubor: docs/04-modules/planning-neg-sell-strategy.md — cíle, slovník, časová osa dne, v32–v35, návrh v36+, TČ/TUV podle typu dne, bazén, UI curtail/reg 340, roadmap, SQL ověření.
Rozhodnutí home-01 (souhrn v docs/06-open-questions.md): rampa/T odvozené z PV B (bez fixních 80 % v LP); TČ ne v pre-neg exportu; bazén min 4 h/den + Shelly; spirála Loxone; workshop UI flex zátěží před v37 (§ 9.1 strategie).
2026-05-28 — Večerní export: dynamický Wh push + hysteresis (v38)
Problém: _evening_battery_export_push_indices bral jen málo slotů v úzkém pásmu max−0,05 a při řazení podle rozpočtu mohl vynechat dražší 15min (9,5 Kč) a exportovat později levněji (4,8 Kč). evening_early zákaz ge_bat platil jen před prvním push slotem.
Změna (v38): Kandidáti = profitable ∩ peak pásmo v nočním okně (_evening_peak_export_indices, max sell v úseku − degrad — shodně s R__063); push = nejdražší sell desc, dokud kumulované_Wh ≤ push_budget (discharge_slot_buffer, SoC nad min_soc); per_slot = min(BMS, export cap) × účinnost × 0,25 h — počet slotů dynamický (např. ~40 kWh / ~3,4 kWh ≈ 11 slotů u home-01), ne pevné top-3. evening_early = ge_bat=0 pro profitable noční sloty pod peak−0,05 mimo evening_push_ts (i po prvním push). Rolling hysteresis při malé změně peak sell / SoC. (Doplněno ve v39: stejná logika, tag evening-export-soc-balance-v39.)
Soubory: backend/services/planning_engine.py, backend/tests/test_planning_dispatch_milp.py, docs/04-modules/planning.md.
Ověření: pytest … -k evening; tag 2026-05-28-evening-export-dynamic-v38. solver_params.inputs.evening_push_ts — délka ≈ floor(push_budget_wh / per_slot_discharge_wh).
2026-05-28 — SoC bilance: jen bd, ne bd+ge_bat (v39)
Problém: SoC kontinuita odečítala bd + ge_bat, ale z energetické bilance pv + gi + bd = load + bc + ge už platí bd ≈ load + ge_bat při exportu z baterie → pokles SoC ~2× rychleji než BMS ve večerním BATTERY_SELL. v37 kalibrace (discharge_calibration_factor) to jen maskovala.
Změna (v39): SoC rovnice: − bd[t] / discharge_efficiency × interval_h (bez druhého ge_bat). Odstraněno: fn_soc_tracking_bundle, _soc_tracking_bundle, discharge_calibration_factor.
Soubory: backend/services/planning_engine.py, db/routines/R__091_fn_soc_tracking_bundle.sql (drop), backend/tests/test_planning_dispatch_milp.py (SocBalanceDischargeTests), docs/04-modules/planning.md.
Ověření: SocBalanceDischargeTests::test_export_slot_soc_drop_not_double_ge_bat; MCP po deploy: planner_build_tag = 2026-05-28-evening-export-soc-balance-v39, drift plan_soc vs actual_soc při večerním výboji.
2026-05-28 — SoC tracking + discharge_calibration_factor (v37, nahrazeno v39)
Problém: LP bilance SoC při výboji klesala o 15–25 % rychleji než BMS → méně BATTERY_SELL ve večerní špičce, energie zbytečně „na zítra“.
Změna (v37): ems.fn_soc_tracking_bundle + _soc_tracking_bundle v rolling replanu; discharge_calibration_factor násobí (bd + ge_bat) jen v rovnici kontinuity SoC (solve_dispatch). Konstanty: error práh 3200 Wh, min výboj 1000 Wh, factor clamp 0.5–1.2.
Soubory: backend/services/planning_engine.py, db/routines/R__091_fn_soc_tracking_bundle.sql, docs/04-modules/planning.md.
Ověření: SocTrackingDischargeCalibrationTests; MCP po večerním výboji: solver_params->'inputs'->>'discharge_calibration_factor', |plan_soc − actual_soc| < 8 % po ~2 h (cíl < 5 % po doladění). Tag v37. → Root cause opraven v39; kalibrace zrušena.
Problém (v36f): BA81 — skip_pv_store nestačil: fixed_pv_b_export_cap držel ge_pv ≤ pv_b → curtail pole A. home-01 rolling — prázdné neg_evening_* (D−1 večer mimo horizont), SoC ~29 % místo ~20 % před sell<0.
Změna (v36g): Fixed pre-neg: ge_pv ≤ pv_surplus (A+B). Spot neg: kotva i na first_neg−1 + výboj ve všech kladných sell slotech před 1. sell<0 (ne jen D−1 večer).
Ověření: test_ba81_fixed_morning_exports_pv_a_not_curtail, test_rolling_horizon_drains_to_reserve_before_first_neg; tag v36g.
Deploy verified (2026-05-28, MCP user-postgres-ems): Všechny aktivní rolling runy (home-01, BA81, KV1, hulin-bess) mají planner_build_tag = 2026-05-28-neg-prep-window-v36g. BA81 run 19604: před 1. sell<0 (29.5. 10:15 Prague) u 19 slotů s PV přebytkem pv_a_curtailed_w = 0, |grid_setpoint_w| = pv_surplus (0 curtail/export mismatch). home-01 run 19560: neg_evening_reserve_soc_anchors délka 2 (kotvy 28.5. 23:45 a 29.5. 10:00 Prague, target_reserve_soc_wh 12 800), večerní výboj k ~20 % SoC před neg oknem.
2026-05-28 — Fixed tarif: export FVE před sell<0 (v36f)
Problém: BA81 (fixed, sell>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 < 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<0 při sell≥0. export_mode: BATTERY_SELL jen když ge_bat je významný (≥500 W), jinak PV_SURPLUS (oprava matoucího labelu při ~600 W exportu).
2026-05-28 — KV1 fixed + block_export (v36e)
Kód: planning_engine.py tag 2026-05-28-neg-prep-window-v36e; R__063_fn_load_planning_slots_full.sql.
Problém: KV1 (fixní buy ~6,35, jen PV A, block_export_on_negative_sell) — od v34/v36 logiky pro spot/home-01: ráno curtail místo exportu do site; večer jen jeden discharge slot (sell peak 6,57 vs buy 6,35). BA81 má pole B (fixed_pv_b_export_cap) a nižší buy → chová se správně.
Změna: skip_pv_store_block pro fixed+block_export bez PV B při sell≥0; večerní evening_peak_export_ts + profitable export pro všechny kladné sell sloty v nočním okně; SQL maska allow_discharge_export stejně pro KV1 večer.
Ověření: PreNegativeSellExportTests (s purchase_pricing_mode=fixed); po deployi KV1 plán: odpoledne PV_SURPLUS / export, večer více BATTERY_SELL slotů.
2026-05-28 — Přípravné okno neg dne (v36 / v36b / v36d)
Kód: backend/services/planning_engine.py — tag 2026-05-28-neg-prep-window-v36d.
Změna (v36): Bod T, pre-neg per den (cushion A+B), večerní neg_evening_before_neg_slots.
Změna (v36b): Kotva neg_evening_reserve_soc_anchors — SoC na konci večera D−1 ≤ reserve_soc_wh (+ slack). Chyba: slack horní mez = soc_max − reserve → LP nechal ~50 % SoC (penalizace 4 Kč/Wh na obří slack).
Změna (v36d): Slack max 400 Wh, penalizace 55 Kč/Wh; večerní ge_bat shortfall bez filtru profitable export; exportní podlaha u neg_evening_before_neg_ts = min_soc (ne arb_base). Kotva jen večer D−1 (ranní slot před 1. sell<0 nekoliduje s prep rampou).
Ověření: NegSellPrepWindowV36Tests (vč. test_evening_reserve_soc_near_reserve_after_discharge); MCP: planner_build_tag = v36d, battery_soc_target_pct u kotvy ≤ ~22 % (reserve 20 % + slack).
2026-05-28 — Rampa SoC z PV B, bod T (v35)
Kód: backend/services/planning_engine.py — tag 2026-05-28-neg-sell-b-ramp-v35.
Změna: _neg_sell_day_phases počítá soc_need[t] zpětnou projekcí jen z PV B; prep cíle = rampa (ne fixních 80 %). t_detach, E_surplus_after_t v solver_params.inputs. Prep hold na soc_target[t] z rampy; po T měkké NEG_SELL_POST_DETACH_BCPV_DISCOURAGE. Cushion v33: cíl z rampy, usable jen z B.
Ověření: pytest tests/test_planning_dispatch_milp.py -k "NegSell or PreNeg or LoadFirst"; MCP solver_params.inputs.neg_sell_day_meta.
2026-05-28 — Tvrdý load-first v LP (v34)
Problém: V sell<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<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<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<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 < 17).
Změna (tag 2026-05-28-night-export-window-midnight-v30): _night_export_window_segments — okno ≥17h + 0–5h Prague, konec při pv_a+pv_b > load + 500 W. _evening_peak_export_indices / push / evening_early používají jeden max sell v nočním úseku (přes půlnoc). Po východu FVE žádný tvrdý push baterie.
Ověření: pytest … -k night_window_includes_midnight or midnight_higher_sell · planner_build_tag v30.
2026-05-28 — FVE při kladném sell: solver místo pv_store curtail (v29)
Problém (home-01 odpoledne): ge_pv = 0 když sell < max(future_sell) (např. 3 Kč vs. večerních 6 Kč) při plné baterii → curtail celého pole A. Záměr „držet na večerní peak“ měl platit pro baterii (ge_bat), ne blokovat export FVE.
Změna (tag 2026-05-28-pv-positive-sell-solver-v29): skip_pv_store_block u spotu pro sell ≥ 0 + PV přebytek (home-01 i KV1). Tvrdý ge_pv = 0 zůstává pro sell < 0 (a fixní tarif dle fixed_pv_b_export_cap). Večerní export baterie beze změny (v28).
Ověření: pytest … -k Home01PvStoreValueTests · planner_build_tag v29 · odpolední slot: export FVE (grid_setpoint_w < 0), ne plný curtail.
2026-05-28 — večerní export: plný site cap (v28)
Problém (v27): Push používal ge_bat ≤ (max_discharge−load)/2 kvůli LP limitu bd+ge_bat ≤ BMS při bilanci bd≈load+ge_bat — plán ~8 kW místo až 13,5 kW (home-01).
Změna (tag 2026-05-28-evening-peak-full-export-v28): Push cap min(export_cap, max_discharge−load); v evening_push_ts BMS load + ge_bat ≤ max_discharge místo bd+ge_bat. Deye realtime dál řídí load-first na zařízení.
Ověření: pytest … -k evening_push_export_near_site_cap_home01 · planner_build_tag v28 · |grid_setpoint_w| ≈ 13,5 kW při typickém večerním load ~1,8 kW.
2026-05-28 — večerní export: oprava home-01 bez prodeje (v27)
Problém (v26, home-01 run 17010): Večer baterie vybíjela jen do domu (export_mode NONE, grid_setpoint_w 0). Dva důvody: (1) evening_early (ge_bat=0) platilo i po nejvyšším sell slotu, takže 19–21 h nemohly exportovat; (2) při drahém importu (buy ≫ ranní ref_buy) bilance s gi≈0 dává ge_bat≈0 při bd≈load, takže tvrdý push na ge_bat bez bd≥load+ge_bat byl neřešitelný / ignorovaný; terminal SoC dále tlumil z_export.
Změna (tag 2026-05-28-evening-peak-full-export-v27): evening_early jen pro sloty před min(evening_push_ts); push: ge_bat cap ≈ (max_discharge−load)/2, bd+ge_bat≥load+ge_bat, z_export=1; vyšší bonus EVENING_PUSH_Z_EXPORT_BONUS_CZK. Detail: planning.md.
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 (≥ max−0,05): plný ge_bat; počet slotů = Wh rozpočet, řazení sell desc |
Není jediný slot; není široké peak−degrad pro push |
_dispatch_grid_setpoint_w |
grid_setpoint_w z ge / ge_bat pro Deye reg 143 |
— |
Ověření: pytest … -k evening_peak_battery_export_at_site_cap · planner_build_tag = v26.
2026-05-28 — reg 340 cap z výkonu střídače, min dle firmware (V082)
Změna: asset_inverter.deye_reg340_max_solar_w / deye_reg340_min_solar_w; fn_inverter_pv_a_max_w bere strop z DB sloupce (home-01 32 000 W, ostatní Deye 65 000 W), ne součet Wp polí — studené panely mohou překročit nominál. compute_pv_a_reg340_max_solar_w(..., min_w=) — spodní limit jen pro kladné hodnoty (home-01 min 400 W).
Ověření: select ems.fn_inverter_pv_a_max_w(<deye-main id>); · pytest backend/tests/test_control_exporter_reg340.py.
2026-05-28 — reg 340 jen když plán curtailuje / exportuje / nabíjí
Změna: plan_skips_deye_reg340_write v setpoints.py — bez FC 0x10 na reg 340, pokud slot nemá export, nabíjení baterie ani pv_a_curtailed_w (Deye řídí PV A přes 108/109/142).
Ověření: pytest backend/tests/test_control_exporter_reg340.py.
2026-05-28 — dvoufázová SoC před buy<0, PV A curtail jen v buy<0 (v25)
Požadavek: (1) PV A omezení jen při buy<0 — raději import se ziskem než „zdarma“ ze střechy. (2) Před buy<0 dostatečně nízké SoC (vejde import v okně + PV B + rezerva na odpolední sell<0). (3) Nejpozději při posledním sell≥0 před buy<0 baterie ~100 % (bez exportu — PV do bat). (4) Ranní sell<0 před buy<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<0, NEG_SELL_CURTAIL jen buy<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<0.
Oprava (tag 2026-05-28-pre-neg-batt-discharge-v23): _pre_neg_buy_discharge_indices — sloty t < 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<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<0 = export, bat>0 = nabíjení). Chování ale „opačně“:
- Před buy<0 (05:30–07:00, buy≥0): nabíjení z PV/sítě místo přípravy kapacity.
- Při buy<0 (12:15–12:45): export do sítě místo importu — ventil
w_pv_b_ventu sell<0 platil i když buy<0.
Oprava (tag 2026-05-28-buy-sell-split-v22):
- Před
first_neg_buy_idxa buy≥0: tvrdébc_pv=bc_gi=0jen vis_daytime_pv_surplus_slot(SQL); jinak měkká penalizacePRE_NEG_CHARGE_PENALTY. - sell<0 a buy≥0: export pole B / curtail A (v21b), bez
neg_sell_socshortfallu v buy<0 slotech. - buy<0: tvrdě
ge=ge_pv=ge_bat=0+ měkkýneg_buy_charge_shortfall(tlak nabc_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:00–11: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:00–12:45 buy<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: v19–v19c opakovaně Solver: Infeasible na home-01 (ověřeno proti MCP run 16674 — buy<0 od 11:00, ne 13:00). Vrstvené Python patch bez reprodukce na živých slotech.
Rozhodnutí: Revert celé v19 Python vrstvy (pre-neg discharge, bc_pv=0 před buy<0, neg-buy shortfall). Zůstává stabilní základ:
- v17:
bc_gi=0při sell<0+PV+buy≥0;ge_pv ≤ pv_bpř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_export → ge_bat=0 (sell<0) vs z_export → ge_bat≥1 → Infeasible.
Oprava (tag 2026-05-27-pre-neg-buy-strategy-v19c):
neg_buy_soc_underfilljen na poslednímbuy<0slotu horizontu.pre_neg_buy_discharge_tsjen přisell ≥ 1(ne SQL discharge maska se záporným sell).- Třetí retry:
relaxed_neg_buy_pressure(vypne měkké shortfall, ponechábc_pv=0př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:45–11:30 nabíjení z PV A do ~98 % ještě před buy<0 (13:00); (3) v buy<0 baterie plná → žádný import; (4) neg_sell_soc_underfill tlačilo na soc_max už v ranním sell<0 okně.
Oprava (tag 2026-05-27-pre-neg-buy-strategy-v19):
- Noční výboj:
pre_neg_buy_discharge_ts— shortfall + pushge_batna site cap, bonusz_export, export podlahamin_soc(ne safety ramp). bc_pv[t]=0pro všechny sloty předfirst_neg_buy_idx(i kdyžt in charge_slotszsell<0+PV).neg_sell_soc_underfilljen pofirst_neg_buy_idx— před záporným nákupem nehonit soc_max.neg_buy_soc_underfill+neg_buy_grid_shortfallvbuy<0slotech — tlak na soc_max a maxbc_gize sítě.
Ověření: pytest backend/tests/test_planning_dispatch_milp.py — po deploy replan home-01: tag v19; noc ge_bat ~13,5 kW; před 13:00 SoC pod max; 13:00–14:45 import + nabíjení k 100 %.
2026-05-27 (h) — export push z DB limitů, bez hardcoded 8000 W (v18)
Problém: EVENING_BATTERY_EXPORT_MIN_W a PRENEG_MORNING_EXPORT_MIN_W = 8000 W v kódu brzdily home-01 na 8 kW místo site_grid_connection.max_export_power_w (13,5 kW); u KV1 náhodou sedělo. EVENING_PEAK_FULL_POWER_TOP_K = 6 arbitrární.
Oprava (tag 2026-05-27-site-export-cap-from-db-v18):
- Smazány konstanty
EVENING_BATTERY_EXPORT_MIN_W,PRENEG_MORNING_EXPORT_MIN_W,EVENING_PEAK_FULL_POWER_TOP_K. - Helper
_battery_export_cap_w(battery, grid)=min(max_discharge_power_w, max_export_power_w)z DB. - Ranní/večerní push
ge_bat >= export_push_w * z_exportpouží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):
- Nákup ze sítě 18 kW v 09:15–09:45 za buy 1,1–1,2 Kč: R__063 přidává
allow_charge=truei prosell<0+pv_surplus>0(= "povolit PV nabíjení aby pole A nešlo do mínusu"), alet in charge_slotsv Pythonu pak otevřelo ibc_gi(grid→bat) za pozitivní buy → ztráta ~25 Kč. - Export pole A v sell<0 oknu (11:00–14:45):
ge_pvmohlo 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). - Večerní prodej jen 8 kW místo 13,5 kW:
EVENING_BATTERY_EXPORT_MIN_W = 8000byl 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):
- bc_gi=0 v
sell<0+pv_surplus>0slotech s buy≥0 (mimocharge_slotsuž zůstává). Důvod:t in charge_slotsz PV důvodu není ekvivalentní "povolit nákup ze sítě". Arbitráž ze sítě (cheap buy → peak sell) zachována dokudpv_surplus=0(= testtest_vt_nt_cycle_evening_battery_sell). - ge_pv ≤ pv_b_forecast_w v
sell<0slotech 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. - 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 pokudlen(evening_push_ts) ≥ 7(= multi-slot peak okno, ne 1-slot regresní testy).
Ověření: pytest backend/tests/test_planning_dispatch_milp.py — 87 passed (1 pre-existing fail). Po deploy + replan home-01:
- 09:15–09:45 bez import 18 kW (bc_gi=0).
- 11:00–14:45
curtail_a ≈ pv_a − epsilon,ge_pv ≤ pv_b. - Večerní peak (20:30, 20:45, 21:00, 22:00) ge_bat ≥ 13 500 W → kratší okno, vyšší marže.
2026-05-27 (f) — zjednodušená strategie pro buy<0 okno (v16, revert v14+v15)
Problém v14/v15 (run 16622, 16636, 16642): Vrstvy soft penalty (cap+slack, PV charge suppressed penalty) LP nedonutily vybít baterii ani omezit PV pumping. LP přijímal sloupec slack 24 kWh × 50 Kč/kWh = 1190 Kč a baterii nabíjel z ranního PV (10:30 SoC=95 %), pak v buy<0 okně (13:00–14:45) curtail pole A 5–9 kW + export pole A do mínusu.
Strukturální root cause (3 vrstvy):
- R__063
allow_charge=falseze SQL Pythonskýsolve_dispatchignoruje pro PV charging (bc_pv ≤ pv_surplusi prot not in charge_slots). discharge_export_slotsv nocifalse(R__063) → LP nemá cestu jak baterii vybít přes ge_bat.acquisitionv 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:
- Tvrdé
bc_pv[t] = 0pre-first_neg_buy_idx (slots kdet not in charge_slots): PV poteče do gridu (sell≥0) nebo curtail, ne do baterie. R__063 už prosell<0+pv_surpluspřidáváallow_charge=true(=t in charge_slots), takže pole A vsell<0slotech může nabíjet baterii (= nevyhodit do mínusu). - Rozšíření
discharge_export_slotso pre-buy<0sloty se dynamickým prahemsell ≥ 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 sesell > 0. Ekonomická logika: maržesell_t − acquisition_in_neg_buy_window − degradation, a pokudacquisition ≈ 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<0okno: PV B necurtailovat (R__063 už řeší), nabíjet ze sítě (LP samo, buy záporný =t in charge_slots).- Po
sell>0: baterie plná, max prodej. - Večer: prodat zbytek.
Ověření: pytest backend/tests/test_planning_dispatch_milp.py tests/test_planning_charge_slot_selection.py — 87 passed (1 pre-existing fail nesouvisí). Po deploy MCP: select pr.solver_params->'planner_build_tag' = …-v16, plán home-01 25.5.: SoC v 12:45 < 50 %, 13:00–14:45 SoC roste z capu k ~95 %, pv_a_curtailed_w blízko 0 v okně.
2026-05-27 (c) — rezervace SoC pro sell<0 okno + fallback acquisition ≥ 0 (v13)
Problém (home-01 run 16614, tag v12): Aktivní plán pro 2026-05-25:
- 10:30 SoC = 96,9 %, 10:45 SoC = 98,3 % (baterie plná z PV ráno) → odpoledne v
sell<0slotech (13:00–14:45, sell až −1,08 Kč) ge_pv export + curtail pole A 5 kW. Ztráta 6+ Kč. acquisition_pass1 = −0,035(R__063fallback path:(ref_buy_am + ref_buy_pm)/2, ref_buy_pm < 0 protože PM zahrnuje 13:30–14:00 s buy ≈ −0,36 Kč) →two_pass_converged = false.
Oprava (tag 2026-05-27-neg-sell-soc-reservation-v13):
R__063PV vrstva A — rezervace prosell<0okno: před iterátorem vrstvy A spočítatv_neg_window_pv_surplus_wh = sum(min(pv_surplus_w, max_charge_w) * eff * 0.25) FILTER (sell<0, pv_surplus>0). Snížitv_pv_layer_cap_who tuto hodnotu (lower bound 0). Důsledek: předsell<0oknem se nabíjí jendeficit − neg_window_pv_wh; do okna doráží baterie nenaplněná asell<0PV slot ji dorovná místo exportu / curtailu pole A.R__063fallback acquisition: kdyžv_est_grid_wh = 0amin(buy) FILTER (allow_grid_charge AND buy>=0)je NULL, místo avgref_buy_am/pm(může být záporný) použítcoalesce(min(buy) FILTER (buy>=0), 0). Navícv_charge_acquisition := greatest(v_charge_acquisition, 0)jako pojistka — arbitrážní akviziční cena nesmí být < 0.
Ověření:
- Replan home-01 (po redeploy R__063) → 10:45 SoC < 95 %, 13:00–14:45 SoC roste (PV charging), 13:30
grid_setpoint< 0 jen pole B (curtail pole A = 0), bilance:cashflow_czk(13:00–15:00) > 0. acquisition_pass1_czk_kwh ≥ 0,two_pass_converged = true.
2026-05-27 (b) — acquisition: vyloučit záporný OTE buy z váženého průměru
Problém (home-01 run 16588): two_pass_converged=false, acquisition_pass1≈−0.035 (pass1 nabíjení v buy<0 slotech), pass2≈0.88. Noční grid 4,8 Kč už v plánu není (maska B OK), ale two-pass a arbitrážní marže exportu baterie byly křivé.
Oprava: R__063 — vážená acquisition ve filtru B a v charge_acquisition_buy_czk_kwh jen z allow_grid_charge s buy_price >= 0. planning_engine._recompute_charge_acquisition_from_results přeskočí buy<0.
Ověření: po redeploy replan home-01 → two_pass_converged=true, |acq1−acq2| < 0.05.
2026-05-27 — self-konzistentní grid maska B + ekonomický rozpad plánu (v12)
Problém (home-01, run 16522, tag v11): Noční grid nabíjení (23:30–23:45, buy ~4,8 Kč) při acquisition_pass1≈4,81 / pass2≈0,84, two_pass_converged=false; 26 slotů export při sell<0.
Oprava (tag 2026-05-27-self-consistent-grid-mask-v12):
R__063: iterativní filtr vrstvy B (spot) + sloupcepv_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řesfn_planning_run_commit.planning_engine.py: post-processing ekonomiky,solver_params.objective_termsrozšíř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=0dokud není plná baterie; při plné jenge_pv ≤ pv_b(w_pv_b_vent_neg) + penalizaceNEG_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>0přisell<0, pokudmin_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:
- Spot při
sell<0:skip_pv_store_blockkvůlipv_bpovoloval export i s prázdnou baterií. - R__063 večerní maska spot:
sell > ref_buy— ve slotu častosell < buy, večerní export dnes vypnutý. - Večerní
ge_batpush jen 50 % výkonu vs. terminal SoC shadow.
Oprava (tag 2026-05-25-home01-neg-sell-evening-v10):
- Spot
sell<0:ge_pv/gejen pokudsoc[t-1] ≥ soc_max − headroom(binárkaw_pv_full_neg). - R__063: večerní peak u spotu jako u fixního tarifu (denní max výkupu).
PEAK_EXPORT_SHORTFALL80 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_contextvracímarket.purchase_pricing_mode/sale_pricing_modezsite_market_config.- Při
sell < 0apurchase_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_likezůstává jen pro drahý import /charge_acquisition(heuristika + DBfixed).
Ověření: pytest …::NegativeSellPvChargeTests::test_ba81_fixed_purchase_nt_vt_buy_spread_neg_sell_no_export; po deploy + replan BA81: žádný grid < 0 při sell < 0 v MCP.
2026-05-25 (l) — Plán 25. 5.: BA81 neg. výkup bez exportu, KV1 ranní curtail
Problém (MCP plán run 16346–16350, tag v7): KV1 06–08 h masivní curtail FVE (plná baterie, ge_pv=0 z pv_store). BA81 při sell<0 export ~10 kW místo nabíjení. Večer slabý export u KV1/home-01 (spot: sell < buy).
Oprava (tag 2026-05-25-neg-sell-no-export-fixed-v8):
- Fixní tarif (BA81): při
sell < 0tvrděge = 0(jako KV1 s block_export) — přebytek jen baterie/curtail. fixed_pv_b_export_capjen přisell ≥ 0(po neg. okně export B).- KV1:
skip_pv_store_blockpři kladnémsell+ 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):
- v5:
ge_pv = 0z pv_store připv_b > 0→ opravage_pv ≤ pv_b. - v6 (skutečný blocker u BA81):
deye_gen_microinverter_cutoff_enabledspolečně ssell < 0vynucovaloge == 0(podmínkaz_gen_cutoff is not None). Při plné baterii nelze nabít ani exportovat přebytek pole B → Infeasible. BA81 má v kontextusoc_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<0
Problém: Plán ukazoval slabé nabíjení/vybíjení (KV1, BA81) přestože ekonomika (OTE) favorizovala opak. Ve špičkách MILP nevybíjel baterii naplno; noc BA81 držela SoC na rezervě bez exportu; záporný výkup neplnil FVE do baterie.
Změny:
| Oblast | Co | Proč |
|---|---|---|
| R__063 — exportní maska | Místo pevného vyloučení 00–04 na den prvního sell<0: slot vynechat z rozpočtu Wh jen pokud existuje pozdější slot tentýž den (před prvním sell<0) s sell > sell_slot + degradace. |
Řídit se OTE cenami, ne hodinami. BA81 noc může exportovat; home-01 půlnoc se vynechá, pokud je lepší sell ráno. |
| R__063 — fixní tarif | Discharge kandidáti: sell > buy + degradace (ne jen sell > degradace). |
U BA81/KV1 export jen když je výkup nad fixním nákupem. |
| R__063 — PV vrstva A | allow_charge z FVE při sell < 0 bez filtru future_sell_lookahead; filtr „drž na večerní peak“ jen pro sell ≥ 0. |
V záporném výkupním okně nabít z FVE (KV1 block_export). |
| LP — export shortfall | Penalizace nevyužitého exportu na ge_bat, ne na ge; pro všechny allow_discharge_export sloty s kladnou marží (sell > acquisition resp. sell > buy + degrad u fixed). |
Dříve jen high_sell_slot (globální max lookahead) → většina večerních slotů bez tlaku na vývoz. |
| LP — ge_bat push | Min. ~8 kW export z baterie ve všech ekonomicky výhodných discharge slotech (ne jen večer/ráno seznam). | Plán má odpovídat „vylije co dá síť“ ve špičkách. |
| LP — záporný sell + block_export | charge_slots rozšířeny o sloty sell<0 s PV přebytkem; měkká penalizace pv_charge_shortfall (bc_pv vs přebytek FVE). |
Postupné nabíjení / curtail místo plné FVE do baterie. |
Soubory: db/routines/R__063_fn_load_planning_slots_full.sql, backend/services/planning_engine.py, backend/tests/test_planning_charge_slot_selection.py, docs/04-modules/planning.md.
Neměněno (záměrně):
reserve_soc_percentu BA81 (30 %) — podlaha pro prodej do sítě; pod ní jen dům. Noc držela 30 % kvůli zakázanému exportu v masce, ne kvůli špatné rezervě.- Ranní export 5–11 před
sell<0, večerní peak ≥17, kotva SoC — beze změny.
Ověření po deployi:
- Flyway repeatable
R__063+ restart backendu. - Rolling replan BA81 / KV1 / home-01.
- MCP: noc BA81 —
allow_discharge_export=truekde není lepší sell později; večerabs(battery_setpoint_w)řádově kW u slotů sexport_mode=BATTERY_SELL. 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:
- Flyway
R__063neaplikovaný na DB → masky bezallow_chargeu záporného výkupu (ch_true=0na celém runu KV1). - Fixed marže:
_slot_profitable_battery_exportpoužívalbuyv slotu (predikce 4,08 Kč) místocharge_acquisition(~3,09) → večerní export vypnutý i přisell3,7. ge_bat ≤ max_export × z_export: solver volilz_export=0→ge_bat=0navzdory push.- 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 15810 — max_chg ≈ 3275 W, allow_grid_charge = 0 na všech slotech. Noc 00–04 jen import pro dům (~100 W), žádné NT nabíjení ze sítě. HW limit BA81 je 6250 W (bms_max_charge_w), ne 18 kW.
Příčina: V R__063 vrstva B (grid) běžela jen pro purchase_pricing_mode <> 'fixed'. BA81 má fixed → masky povolily jen PV vrstvu A (Wh rozpočet rozdělený přes denní FVE sloty → postupné ~3 kW).
Oprava: Pro fixed + existuje arbitráž (sell > buy + degrad) → stejná AM/PM logika grid slotů jako u spotu, řazení podle času slotu (slot_ord), před export_window_start.
Ověření po flyway migrate + replan:
select count(*) filter (where (m->>'allow_grid_charge')::boolean) as grid_slots
from ems.planning_run pr, jsonb_array_elements(pr.solver_params->'masks') m
where pr.site_id = (select id from ems.site where code='BA81') and pr.status='active';
-- očekáváno > 0
select max(pi.battery_setpoint_w), max(pi.grid_setpoint_w) filter (where pi.grid_setpoint_w > 1000)
from ems.planning_interval pi
join ems.planning_run pr on pr.id = pi.run_id
where pr.site_id = (select id from ems.site where code='BA81') and pr.status='active';
-- battery/grid nabíjení řádově k 6250 W v NT slotech
2026-05-24 (d) — BA81: grid jen 1 slot (globální export okno)
Problém: Run 15820 — mírné zlepšení (1× ~4,5 kW grid+bat o půlnoci), ale 00:45–05:45 allow_charge=false, max nabíjení pořád ~3,3 kW z FVE.
Příčina: v_export_window_start = min přes celý horizont (včerejší večerní sell 3,7 → čas ~22:15). Grid vrstva B řadí „před oknem“ vůči tomuto jednomu času → dnešní NT sloty (00–06) už jsou „po okně“ a nedostanou allow_grid_charge.
Oprava: Sloupec export_window_start_at per kalendářní den (Prague); grid AM/PM i buy_min_next_n používají wk.interval_start < wk.export_window_start_at.
Deploy: flyway migrate (R__063) + replan.
2026-05-24 (e) — BA81: FVE 13 kW → nabíjení jen ~3 kW (curtailment)
Problém: Run 15826 — pv≈13 kW, battery_setpoint≈3,3 kW, pv_a_curtailed≈9 kW (08:00–08:45). allow_charge=true, ale solver škrtí FVE místo plného nabíjení.
Příčina:
CURTAILMENT_PENALTY = 0,001 Kč/Whvs degradace nabíjení → LP radějicanežbc_pv.pv_charge_shortfalljen přiblock_export_on_negative_sell(KV1) — BA81 má false → žádný tlak nabc_pv.- SoC v plánu stagnuje ~52 % při záporném výkupu, zbytek jde do curtailment.
Oprava (planning_engine.py):
pv_charge_shortfallpro všechny slotysell<0+allow_charge+ PV přebytek >500 W.- Penalizace 50 Kč/kWh.
- Tvrdé
ca ≤ pv_a_forecast − bc_pvv okně záporného výkupu (nejdřív nabít, pak škrtit).
Deploy: restart backend (SQL beze změny) + replan.
2026-05-24 (f) — BA81: jen první slot sell<0 nabíjí 6 kW, další 1–2 kW
Problém: Run 15838 — 06:15 Prague ~6,1 kW, 06:30–07:30 ~1,4–2,2 kW, 07:45–08:45 0 kW + curtail ~9 kW, 09:00+ znovu ~3 kW. Uživatel: „jen u prvního slotu se zápornou cenou“.
Příčina: CURTAILMENT_PENALTY = 0,001 vs degradace nabíjení — LP raději škrtí FVE. Oprava (e) pomohla jen prvnímu slotu (shortfall). Omezení ca ≤ pv_a − bc_pv bylo špatně (load-first: pv_a_net už závisí na ca). SoC v plánu stála ~51 % uprostřed okna, zbytek do curtailment.
Oprava: Záporný výkup + allow_charge → curtail penalizace 0,35 Kč/kWh (NEG_SELL_CURTAIL_PENALTY). Shortfall nabíjení 80 Kč/kWh. Odstraněno ca ≤ pv_a − bc_pv.
Deploy: jen backend restart + replan.
2026-05-24 (g) — BA81: plateau ~51 % SoC + curtail (run 15848/15849)
Problém: Po replanu stále 06:15 ~6 kW, 06:30–07:30 ~1–2 kW, 07:45–08:45 0 kW + curtail ~9 kW, SoC plán ~51 %, pak znovu ~3 kW. solver_params bez planner_build_tag → nasazený backend pravděpodobně bez oprav (e)/(f).
Příčiny (MCP + kód):
charge_slotsv Pythonu doplňovalsell<0jen přiblock_export_on_negative_sell(KV1). U BA81 (false) platily jen masky z DB → bez shortfall penalizace, i když R__063 nastavíallow_chargepozději.safety_soc_target_whz SQL roste jen k ~reserve + noční baseload (~50 % SoC v poledne). Jakmilesoc ≥ safety, solver nemá motivaci dobít ksoc_maxv okně záporného výkupu (raději curtail / večerní export).skip_pv_store_blockupv_b+ fixní tarif: LP smí exportovat FVE přisell<0místo nabíjení (home-01 logika nepatří na BA81).
Oprava (planning_engine.py):
charge_slots|= všechny slotysell<0+ PV přebytek > 500 W (jako R__063 ř. 787–791).- V okně
sell<0+charge_slots: safety deficit cílí namax(safety_sql, 92 % soc_max). - Fixní tarif:
ge_pv ≤ pv_b_forecast_wpřisell<0;skip_pv_storejen pro spot, ne fixed. - Objective: odměna
bc_pvpřisell<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:
_slot_profitable_battery_export: u fixního tarifu porovnávalsell > acquisition + degrad(BA81 acq ~3,61 → potřeba sell > ~3,91). Správněsell > buy + degradjako v R__063.- KV1 večer: SQL večerní maska vyžadovala
sell > buy(6,35 vs 3,7) →allow_discharge_export = false. - LP:
ge_bat >= export_push * z_export— solver nechalz_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<0tentýž den:post_neg_pv_topup— dobití z FVE nasoc_maxpř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í:** …