Files
ems/docs/planning-changelog.md
Dusan Vojacek 904c318532
Some checks failed
CI and deploy / migration-check (push) Failing after 18s
CI and deploy / deploy (push) Has been skipped
preorita nabijeni pred skrcenim
2026-05-23 22:50:13 +02:00

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


Šablona pro další záznamy

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

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