Fix SoC balance on battery export and improve evening push (v39).
SoC continuity now deducts only bd (ge_bat was double-counted via energy balance), which stopped the plan from draining ~2× faster than BMS during evening BATTERY_SELL. Also ships dynamic evening push budget + rolling hysteresis (v38), drops unused fn_soc_tracking_bundle, and adds tests/docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
- **SQL-first:** horizont a sloty z DB funkcí (`fn_planning_horizon_end`, `fn_load_planning_slots_full`, …); viz **`CLAUDE.md`** → sekce *SQL-first a read-model*.
|
||||
- **Dynamický horizont (jen OTE):** konec plánu z **`ems.fn_planning_horizon_end(site_id, horizon_start)`** (výchozí strop **36 h**, minimum pro rolling **1 h** – obojí jako defaultní argumenty v SQL, úprava přes repeatable migraci). Pomocná `ems.fn_last_effective_ote` vrací konec posledního OTE intervalu. Rolling replan při `NULL` přeskočí; denní plán použije krátký (1 h) fallback v Pythonu. Sloty v solveru jsou bez predikovaných cen v rámci tohoto horizontu.
|
||||
- **Terminal SoC shadow price:** v objective je člen `−(avg_buy_prvních_24h × planner_terminal_soc_value_factor / 1000) × soc[T−1]` (Kč), kde faktor je **`ems.asset_battery.planner_terminal_soc_value_factor`** přes **`ems.fn_planning_site_context`** (default v DB **0.9**); viz sekci *Tuning pro malé baterie* níže. Účel: konec horizontu nemusí končit zbytečně vyprázdněnou baterií (receding horizon).
|
||||
- **SoC kontinuita a export z baterie:** `soc[t]` klesá při **`bd[t]` i `ge_bat[t]`** (vybíjení do domu i do sítě). Bez `ge_bat` v bilanci SoC LP „exportovalo“ bez vybití — arbitrážní dump v pozdních slotech místo ranního peaku.
|
||||
- **SoC kontinuita a export z baterie:** `soc[t]` klesá při **`bd[t]`** — výkon vybíjení na AC sběrnici z energetické bilance `pv + gi + bd = load + bc + ge`. Při exportu z baterie je v `bd` už započten i tok do sítě (`ge_bat` je součást `ge`); **`ge_bat` se v SoC znovu neodečítá** (dříve double-count → plán klesal ~2× rychleji než BMS ve večerním exportu). Tag `2026-05-28-evening-export-soc-balance-v39`.
|
||||
- **Masky `allow_charge` / `allow_discharge_export` (tenký anti-mikrocyklus):** generuje `ems.fn_load_planning_slots_full` (`R__063`). Ekonomiku primárně řídí LP podle efektivních cen; masky jen omezují počet slotů pro grid nabíjení / export baterie.
|
||||
- **PV-surplus (vrstva A):** ranking dle **`store_score DESC`** = `future_sell_opportunity − sell − max(0, buy−sell)`; jen sloty s `sell ≥ buy − degradation`. Kumulativní PV pokrývá `grid_target` (deficit SoC, nad `reserve_soc` bez násobení `charge_slot_buffer`). Zbytek → `allow_charge=false` (PV jen do sítě / `bc ≤ pv_surplus` v LP).
|
||||
- **Grid ze sítě (vrstva B, před FVE):** výchozí **AM/PM 50/50** z `grid_target × charge_slot_buffer` (do `soc_max`); **nevyčerpaný AM Wh přejde do PM** (`R__063`). **Spot:** výběr **nejlevnější `buy`** (den plánu → před exportním oknem → `buy ASC`); navíc všechny sloty s **`buy < 0`** → `allow_grid_charge`. Po výběru AM/PM běží **iterativní self-konzistentní filtr** (vyloučí drahé grid sloty, pokud `pv_charge_wh_ahead + neg_buy_wh_ahead >= 60 %` deficitu SoC; failsafe unlock). Debug: `grid_charge_suppressed_reason`. **Fixní tarif (BA81):** stejný AM/PM rozpočet, ale pořadí podle **`slot_ord`** (buy konstantní), jen pokud v horizontu existuje **`sell > buy + degradation`**; jinak jen PV vrstva A. Cap slotů: `ceil(budget/per_slot_wh) × charge_slot_buffer`. **`charge_acquisition`:** vážený `buy` u `allow_grid_charge` před 1. exportem; two-pass v `planning_engine.py`.
|
||||
@@ -85,24 +85,24 @@ Cíl zůstává **maximální ekonomický užitek v celém horizontu**: prodat (
|
||||
flowchart TD
|
||||
A[LP: globální optimum v horizontu] --> B{slot >= 17h a profitable export?}
|
||||
B -->|sell pod nocnim max - 0.05| C[ge_bat = 0: baterie ne pred spickou]
|
||||
B -->|sell v top pasme max - 0.05| D[evening_push kandidat]
|
||||
D --> E[Seradit sell desc, pridat sloty az do Wh rozpoctu]
|
||||
E --> F[ge_bat >= plny vykon na cap v kazdem push slotu]
|
||||
B -->|profitable + nocni okno| D[push: sell desc az do Wh rozpoctu]
|
||||
D --> F[ge_bat >= plny vykon na cap v kazdem push slotu]
|
||||
C --> G[Vysledek: energie zustane na nejdrazsi vecer]
|
||||
F --> G
|
||||
```
|
||||
|
||||
1. **SQL masky (R__063, vrstva 2)** — které večerní sloty *smí* export z baterie vůbec (`allow_discharge_export`): mimo jiné sloty v pásmu „denní večerní max − degrad“ (SQL), plus globální Wh rozpočet (vrstva 1).
|
||||
|
||||
2. **v27 — zákaz předčasného večerního vývozu** (`evening_early_export_penalty_ts` → tvrdé `ge_bat[t] = 0`):
|
||||
- jen v **nočním okně** (`_in_night_battery_export_window`) a **časově před** prvním slotem v `evening_push_ts`;
|
||||
- jen pokud `sell < max_sell_v_nočním_úseku − 0,05` (v30: max přes půlnoc, ne per kalendářní den);
|
||||
2. **v38 — zákaz předčasného / levného večerního vývozu** (`evening_early_export_penalty_ts` → tvrdé `ge_bat[t] = 0`):
|
||||
- v **nočním okně** pro profitable sloty **mimo** `evening_push_ts` (včetně slotů **po** prvním push — v27 je omezoval jen na čas před prvním push);
|
||||
- pokud `sell < max_sell_v_nočním_úseku − 0,05` (v30: max přes půlnoc);
|
||||
- **nezakazuje** přebytek FVE do sítě (`ge_pv`).
|
||||
|
||||
3. **v24 + v27 — plný výkon v top večerních slotech** (`evening_push_ts`):
|
||||
- kandidáti: profitable ∩ večer ∩ `sell ≥ max_večer − 0,05` (úzké pásmo u **absolutní** večerní špičky, ne široké „peak−degrad“ pro push);
|
||||
- řazení podle **`sell` sestupně**;
|
||||
- přidávat sloty, dokud `kumulované_Wh ≤` rozpočet (`discharge_slot_buffer`, SoC nad `min_soc`);
|
||||
3. **v38 — plný výkon v top večerních slotech** (`evening_push_ts`):
|
||||
- kandidáti: profitable ∩ noční okno ∩ `sell ≥ 0`;
|
||||
- push = nejdražší sloty **seřazené `sell` desc**, dokud `kumulované_Wh ≤ push_budget` (`min(available_soc, exportable_full × discharge_slot_buffer)`; `per_slot` ≈ max_discharge × účinnost × 0,25 h) — **počet slotů dynamický** (ne pevné top-3);
|
||||
- při vysokém SoC může být push slotů víc než 3 (např. 40+ kWh rozpočet → ~9–12 slotů podle `per_slot`);
|
||||
- **rolling hysteresis:** při `|Δ peak sell| < 0,5` Kč a `|Δ SoC| < 5 %` držet `evening_push_ts` z předchozího aktivního runu (`_rolling_evening_push_override`);
|
||||
- **v28 push fyzika:** cap `ge_bat ≈ min(export_cap, max_discharge − load)` a v push slotech BMS `load + ge_bat ≤ max_discharge` (ne `bd+ge_bat`, které dvojí započítávalo export); odpovídá Deye SELL — load z baterie, zbytek do sítě až po site cap;
|
||||
- **výsledek:** jeden nejdražší slot → export řádově kW; další drahé sloty **po** prvním push mohou exportovat dle ekonomiky LP.
|
||||
|
||||
@@ -116,7 +116,7 @@ flowchart TD
|
||||
| Měkká `peak_export_shortfall` → často ~50 % výkonu v mnoha slotech | Na `evening_push` slotech tvrdý push na cap; shortfall na push vypnutý |
|
||||
| `grid_setpoint = gi − ge` → Deye vidí ~0 W při velkém `ge_bat` | `_dispatch_grid_setpoint_w` z reálného exportu |
|
||||
|
||||
**Funkce:** `_evening_battery_export_push_indices`, `_evening_push_discharge_budget_wh`, `_evening_push_battery_export_w`, `_dispatch_grid_setpoint_w` v `planning_engine.py`. Tag: `2026-05-28-evening-peak-full-export-v28`.
|
||||
**Funkce:** `_evening_battery_export_push_indices`, `_evening_early_export_penalty_indices`, `_rolling_evening_push_override`, `_evening_push_discharge_budget_wh`, `_evening_push_battery_export_w`, `_dispatch_grid_setpoint_w` v `planning_engine.py`. Tag: `2026-05-28-evening-export-dynamic-v38`.
|
||||
|
||||
### Arbitráž baterie — účtování mezi sloty (povinné čtení)
|
||||
|
||||
@@ -301,9 +301,11 @@ if ev_session[e].target_deadline and ev_session[e].soc_at_connect_pct is not Non
|
||||
|
||||
### SoC kontinuita
|
||||
```python
|
||||
# battery_discharge = bd (W z baterie na AC sběrnici z bilance pv+gi+bd = load+bc+ge).
|
||||
# ge_bat je součást ge — v SoC znovu neodečítat (v39).
|
||||
soc[t] == soc[t-1]
|
||||
+ battery_charge[t] * charge_efficiency * interval_h
|
||||
- battery_discharge[t] / discharge_efficiency * interval_h
|
||||
+ (bc_pv[t] + bc_gi[t]) * charge_efficiency * interval_h
|
||||
- bd[t] / discharge_efficiency * interval_h
|
||||
|
||||
soc[0] == current_soc_wh # počáteční podmínka z telemetrie
|
||||
```
|
||||
|
||||
@@ -11,7 +11,35 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
||||
|
||||
**Rozhodnutí home-01** (souhrn v [`docs/06-open-questions.md`](06-open-questions.md)): rampa/**T** odvozené z PV B (bez fixních 80 % v LP); TČ ne v pre-neg exportu; bazén min 4 h/den + Shelly; spirála Loxone; **workshop UI flex zátěží před v37** (§ 9.1 strategie).
|
||||
|
||||
## 2026-05-28 — BA81 export A+B + rolling drain (v36g)
|
||||
## 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 ∩ noční okno**; push = nejdražší sloty **sell desc**, dokud `kumulované_Wh ≤ push_budget` (`discharge_slot_buffer`, SoC nad `min_soc`) — **žádné pevné top-3** (počet slotů závisí na SoC, typ. ~4,3 kWh/slot při 17 kW BMS, home-01 export cap 13,5 kW × 0,25 h ≈ 3,4 kWh/slot v LP). `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.
|
||||
|
||||
**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`.
|
||||
|
||||
@@ -19,6 +47,8 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
||||
|
||||
**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.
|
||||
|
||||
Reference in New Issue
Block a user