Files
ems/docs/04-modules/planning-neg-sell-strategy.md
Dusan Vojacek 1429d402e5
Some checks failed
CI and deploy / migration-check (push) Failing after 13s
CI and deploy / deploy (push) Has been skipped
zdokumentovani noveho pohleud na planovani nabijeni
2026-06-01 19:53:04 +02:00

538 lines
24 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Strategie záporného výkupu, FVE A/B, termika a flexibilní zátěže (home-01)
Navazuje na [`planning.md`](planning.md), [`planning-arbitrage-accounting.md`](planning-arbitrage-accounting.md), [`planning-charge-slot-budget.md`](planning-charge-slot-budget.md) (plánovaná náhrada pre-neg cushion), [`planning-changelog.md`](../planning-changelog.md), [`heat-pump.md`](heat-pump.md), [`ev-charging.md`](ev-charging.md).
**Stav:** část je **implementovaná** (v32v40), část je **návrh** (termika, bazén, spirála; **charge-slot-budget** — viz níže). V textu je označeno `✅ hotovo` vs `📋 návrh`.
---
## 1. Cíl produktu (home-01)
| Cíl | Popis |
|-----|--------|
| Baterie v okně `sell < 0` | Dojet na **100 %** (`soc_max`) do konce denního úseku záporného výkupu (Europe/Prague). |
| Pole B (zelený bonus) | Při záporném výkupu smí jít přebytek do sítě (ekonomika bonusu); není curtailable. |
| Pole A (Deye, curtailable) | Po dosažení plánované energetické pohody **nechat dostupné** pro dům a chybu forecastu — ne nutně „vždy škrtat v 80 %“. |
| Ranní kladný sell | Typicky **export celé FVE** do site — nekrást výkon TČ ani fiktivním nabíjením v plánu. |
| Termika | TUV komfort / předehřát / večerní doklep — **uvnitř** vhodných oken, ne v ranním exportním pásmu. |
| Flexibilní sink | Bazén (filtrace), později spirála — sežrat **plánovaný přebytek** místo exportu za záporný sell. |
| EV | Odpoledne; nabíjení po naplnění energetické rampy / v levných slotech. |
---
## 2. Slovník
| Pojem | Význam |
|-------|--------|
| **Okno `sell < 0`** | Souvislé 15min sloty téhož **kalendářního dne** (Prague), kde `effective_sell_price < 0`. |
| **Tail** | Posledních **N** slotů okna (`planner_neg_sell_full_soc_tail_slots`, default **4** = 1 h). Cíl SoC = **`soc_max` (100 %)**. |
| **Prep (v32)** | Všechny `sell < 0` sloty **před** tail. Dnes: plochý cíl **`planner_neg_sell_prep_soc_percent`** (default **80 %**). |
| **Bod T** (`t_detach`) 📋 | První slot (od tail zpět), od kdy **forecast pole B** (po loadu, s limitem nabíjení) **sám** dožene zbytek SoC na 100 %. Nahrazuje fixních 80 %. |
| **`E_surplus_after_t`** 📋 | Integrál plánovaného přebytku FVE (typ. od **T** do `last_sell<0`), který by jinak šel do sítě / curtail — budget pro TČ předehřát, bazén, spirálu. |
| **Pre-neg export (v33)** | Kladné `sell` **před** prvním `sell < 0`: export FVE jen pokud forecast v celém `sell < 0` okně pokryje dobítí na prep cíl (× margin **1,15**). **📋 Plánovaná náhrada:** `pre_window_wh` v [`planning-charge-slot-budget.md`](planning-charge-slot-budget.md) §6. |
| **Load-first (v34)** | Dům z `pv_ld`; při dostatečné FVE žádný fiktivní `grid_import = load` v plánu. |
| **Rampa B + bod T (v35)** | `soc_need` zpět od tail jen z PV B; **t_detach**; `E_surplus_after_t`; uvolnění A po T (měkké). |
| **Reg 340** | Deye *max solar power*`pv_a_forecast_solver_w pv_a_curtailed_w`. |
---
## 3. Časová osa dne (referenční home-01)
```text
Prague
|-----|-----|-----|-----|-----|-----|-----|-----|
06 08 10 12 14 16 18 20
[ A: ranní sell ≥ 0 — export FVE (v33) ]
[ B: sell < 0 — nabíjení bat, T*, TČ, bazén ]
[ C: večerní peak sell — export bat (masky) ]
[ D: EV často odpoledne / večer ]
```
### 3.1 Fáze A — před prvním `sell < 0` (ranní export)
- **Chování plánu (v33):** pokud `_pre_neg_pv_export_forecast_cushion_ok`, sloty v `pre_neg_pv_export_ts` tlačí **export** (`ge_pv`), **`bc_pv = 0`** (FVE ne do baterie).
- **Termika (📋):** **neplánovat** komfortní TČ/TUV v těchto slotech — výkon by kolidoval s exportní strategií (FVE má jít do site).
- **Deye:** load-first na zařízení — dům si vezme z FVE; plán ale může ukazovat export, ne „import pro load“ (viz v34).
### 3.2 Fáze B — okno `sell < 0`
**Energie (v32v35):**
| Období v B | Chování (v35) |
|------------|----------------|
| Začátek okna | Nabít podle **rampy SoC** (`soc_need`) zpět z PV B od tail |
| Střed okna | Od **t_detach**: měkké omezení `bc_pv`; hold/curtail při `soc_prev ≥ soc_target[t]` |
| Tail (posledních N slotů) | Rampa z `soc_need[tail_start]` → 100 % |
**Termika (📋):**
-**primárně zde**, když je dost PV / po bodu **T**, ne v ranní fázi A.
- **Předehřát** v **T** jen pokud je **T** dostatečně brzy a `E_surplus_after_t` je velké.
- **Večerní doklep** TUV (12 h před sprchou) — samostatné pravidlo od `tuv_usage_stats`.
**Bazén (📋):**
- Jen **slunečné** hodiny v rámci B (a ideálně **po T**), **X hodin/den** — promíchání prohřáté hladiny.
### 3.3 Den bez `sell < 0` (📋)
- Přebytek FVE → prodej za **kladný sell** (ne „výmět“).
- **TČ:** topit v slotech, kde **COP × sell** dává smysl oproti prodeji kWh (viz `fn_cop_estimate`, `fn_heat_pump_cost_per_kwh_heat`).
- **Spirála:** spíš **nízká priorita** — každá kWh do spirály je kWh, kterou šlo prodat.
- **Bazén:** volitelně v nejlepších PV slotech, pokud export není ekonomicky nutný.
---
## 4. Implementované vrstvy (v32v35)
### 4.1 v32 — fázované SoC a curtail A ✅
**DB:** `ems.asset_battery` — migrace **`V083__planner_neg_sell_phases.sql`**
| Sloupec | Default | Význam |
|---------|---------|--------|
| `planner_neg_sell_prep_soc_percent` | 80 | **v32 legacy** — od v35 se v LP neřídí (rampa z B). **100** = vypnutí fází (`_neg_sell_phases_enabled`). |
| `planner_neg_sell_full_soc_tail_slots` | 4 | Počet 15min slotů tail před koncem denního `sell < 0`. **0** = bez tail. |
| `planner_neg_sell_vent_min_sell_czk_kwh` | 1 (home-01) | V tail: ventil pole B (`ge_pv`) pokud `sell ≥` práh. **NULL** = jen při plné baterii. |
**Kód:** `backend/services/planning_engine.py`
- `_neg_sell_day_phases()``prep` / `tail` / `none` per slot
- `prep_soc_shortfall`, `prep_hold_met_binary`, měkké `prep_hold_curtail` / `prep_hold_bcpv`
- Výstup: `planning_interval.pv_a_curtailed_w`, `solver_params.masks[].neg_sell_phase`
**Omezení v32 (důvod návrhu v35):**
- **80 %** není odvozené z délky okna ani z forecastu B.
- Curtail A je **měkký** (penalizace ~1 Kč/kWh) — LP může v sousedním slotu znovu nabíjet.
- Hold: `soc_prev ≥ 80 %` na **začátku** slotu, ne dynamická rampa.
**Ověření:** `NegSellSocPhaseTests`, MCP `planning_interval` + `solver_params->'masks'`.
### 4.2 v33 — export FVE před `sell < 0` s forecast pojistkou ✅
**Kód:** `_pre_neg_pv_export_forecast_cushion_ok`, `_neg_sell_day_pv_usable_wh`, `pre_neg_pv_export_ts`.
- Export v kladných slotech před prvním `sell < 0` **jen pokud** usable FVE v celém `sell < 0` dni ≥ potřebné Wh na prep (× **1,15**).
- Jinak LP raději nabíjí z FVE (déšť / slabý forecast v okně).
**Ověření:** `PreNegPvExportForecastTests`, `solver_params.inputs.pre_neg_pv_export_forecast_ok`.
### 4.2b 📋 Plánováno — pre-neg jako energetický rozpočet (charge-slot-budget)
**Stav:** neimplementováno (specifikace 2026-06).
**Problém v33 při zimě / krátkém okně `sell < 0`:** binární cushion často **projde** (optimistický forecast v okně × 1,15) → ranní export FVE i při sell ~23 Kč, přestože **uvnitř** okna energie nestačí na rampu / 100 % tail — velká baterie (home-01) pak přijde do neg okna podnabitá.
**Záměr (souhrn):**
```text
charge_target_at_neg := soc_need[first_neg] (rampa v35/v36, observed SoC)
in_window_wh := sum forecast PV (A+B) v sell<0 sloty dne × η
pre_window_wh := max(0, charge_target_at_neg in_window_wh × reliability)
Před first_neg: allow_charge v nejlevnějších slotech (buy ASC) + PV surplus,
dokud cum_wh < pre_window_wh
Export pre-neg: jen sloty s PV přebytkem, které NEJSOU v charge frontě
```
**Vazby:**
- Rampa / tail / T / curtail A — **beze změny** v LP.
- **v44** (`neg_day_no_grid_before_neg_sell`): plánované **změkčení** — grid před oknem povolen v N nejlevnějších `buy` slotech, pokud `pre_window_wh` výrazně převyšuje `in_window_wh`.
- **v36 per-den bundle** zůstává; `pre_window_wh` se počítá **per pražský den**, ne globálně.
**Detail:** [`planning-charge-slot-budget.md`](planning-charge-slot-budget.md) §4§6, changelog *Plánováno*.
### 4.3 v34 — tvrdý load-first ✅
**Tag:** `2026-05-28-load-first-hard-v34`
- `gi ≤ bc_gi + max(0, max_load pv_forecast)` — při vysoké FVE žádný fiktivní import = load.
- Při `pv_forecast ≥ max_load + 500 W`: `pv_ld ≥ load`.
**Ověření:** `LoadFirstDispatchTests::test_neg_sell_prep_no_fictitious_grid_import_for_load`.
### 4.4 v35 — rampa SoC z PV B, bod T, přebytek ✅
**Tag:** `2026-05-28-neg-sell-b-ramp-v35` (bod T opraven v **v36** — viz níže).
**Kód:** `_neg_sell_pv_b_charge_wh`, `_neg_sell_day_phases` (rampa), `_neg_sell_e_surplus_after_t_wh`, `_neg_sell_day_pv_b_usable_wh` (cushion v33).
- Zpětná projekce `soc_need` jen z PV B; prep `soc_target[t] = soc_need[t]` (ne fixních 80 %).
- **t_detach** = první prep slot kde `soc_need[t] ≤ soc_need[tail_start]`; **E_surplus_after_t** od T do konce okna.
- Prep hold: `soc_prev ≥ soc_target[t]`; po T: `NEG_SELL_POST_DETACH_BCPV_DISCOURAGE` na `bc_pv`.
- `solver_params.inputs`: `neg_sell_b_ramp_v35`, `t_detach_idx`, `e_surplus_after_t_wh`, `neg_sell_day_meta`.
**Ověření:** `NegSellSocPhaseTests::test_b_ramp_t_detach_and_surplus_meta`, MCP `solver_params`.
### 4.5 v36 — přípravné okno neg dne ✅
**Tag:** `2026-05-28-neg-prep-window-v36`
| Problém v35 | Oprava v36 |
|-------------|------------|
| **T** hned na 1. `sell<0` → celý den curtail A | `t_detach``soc_need[t] ≥ 85 % soc_max` + suffix B ≥ zbytek do 100 % |
| Ráno 2. neg dne nabíjí místo exportu | **Pre-neg per den** + cushion **A+B**; `pre_neg_pv_export_slots` pro každý pražský den zvlášť |
| Večer nevybije před zítřejším neg | `neg_evening_before_neg_slots` — výboj večer **D1** |
**Cílová časová osa (např. 27. 5.):**
```text
0709:30 sell ≥ 0 → export FVE (pre-neg, cushion OK)
09:45+ sell < 0 → nabíjení A+B po rampě
~1113 bod T → uvolnění / curtail A, B do domu nebo export
večer 26.5 → vybít bat před neg 27.5 (headroom)
```
**Ověření:** `NegSellPrepWindowV36Tests`, `solver_params.inputs.pre_neg_cushion_by_day`, `neg_evening_before_neg_slots`.
### 4.6 v40 — pozorované SoC pro neg-prep (Plan 5) ✅
**Tag:** `2026-05-29-neg-prep-observed-soc-v40`
| Problém v36 | Oprava v40 |
|-------------|------------|
| Cushion / večerní výboj z **modelového** SoC (řetězení cílů mezi dny) | **`observed_soc_wh`** z telemetrie; žádné `soc_est := soc_target[first_neg]` |
| BMS výš → plán „už mám headroom“ nevidí | Cushion OK pokud `observed_soc ≥ soc_target[first_neg]` |
| Večerní výboj pod exportuje | Rozpočet `max(0, observed reserve night_baseload_buffer)``neg_evening_push_slots` |
**Kód:** `_pre_neg_pv_export_bundle`, `_neg_evening_discharge_budget_wh`, `_neg_evening_before_neg_push_indices` v `planning_engine.py`.
**Ověření:** `ObservedSocNegPrepTests`; MCP `solver_params.inputs.observed_soc_wh`, `neg_evening_export_budget_wh`, `neg_evening_push_slots`.
---
## 5. Specifikace rampy (v35 — reference)
### 5.0 Rozhodnutí produktu (home-01, 2026-05)
| Téma | Rozhodnutí |
|------|------------|
| Rampa / **T** | Odvozené z PV B; **bez** řízení fixním `planner_neg_sell_prep_soc_percent` v LP pro home-01. |
| TČ v pre-neg | **Zákaz** plánovaného topení. |
| Bazén | Min. 4 h filtrace/den, dynamicky navýšit; Shelly; přitop ručně / později. |
| Spirála | Loxone; v38. |
| UI flex | Workshop **před** v37 — viz § 9.1. |
### 5.1 Kotva vzadu (tail — beze změny konceptu)
Pro každý pražský den s `sell < 0`:
```text
indices = všechny sloty t kde sell[t] < 0, seřazené
last_neg = indices[-1]
tail_start = max(indices[0], last_neg - (N - 1)) # N = planner_neg_sell_full_soc_tail_slots
```
Pro `t ≥ tail_start`: cíl `soc_target[t] = soc_max` (případně rampa v tail mezi `soc_detach` a `soc_max` pokud `N > 1`).
### 5.2 Zpětná projekce pouze z pole B
Pro odhad **nabití z B** v slotu `t` (zjednodušený model, stejný styl jako `_neg_sell_day_pv_usable_wh`):
```text
pv_surplus_b[t] = max(0, pv_b_forecast[t] - load_baseline[t] - rezerva_EV_HP)
charge_b[t] = min(pv_surplus_b[t], max_charge_power_w) × charge_efficiency × 0,25 h
```
Zpět od `tail_start`:
```text
soc_need[last_neg] = soc_max
soc_need[t-1] = soc_need[t] - charge_b[t] # clamp ≥ min_soc_wh
```
Výsledkem je **`soc_need[t]`** — požadované SoC na **konci** slotu `t`, kdyby stačilo jen B.
### 5.3 Bod T (`t_detach`) — v36
**Definice (implementováno v36):** první prep slot `t`, kde současně:
```text
soc_need[t] ≥ max(0,85 × soc_max, 0,92 × soc_need[tail_start])
Σ charge_b[t..konec] ≥ (soc_max soc_need[t]) × 1,05
```
**Zrušeno (chyba v35):** `soc_need[t] ≤ soc_need[tail_start]` — platilo vždy na začátku okna.
**Interpretace:**
| Situace | Význam |
|---------|--------|
| **T** brzy po začátku `sell < 0` | Dlouhé okno, B stačí → od **T** uvolnit A pro dům / odchylku |
| **T** těsně před tail | Krátké okno → A potřebné déle, malý `E_surplus_after_t` |
| Aktuální SoC **pod** `soc_need[t]` při replanu | Ještě fáze „honit rampu“ (A+B) |
| Rampa z aktuálního SoC **nedosáhne** tail ani optimisticky | Slabý den — 100 % dnes nejspíš nevyjde |
### 5.4 Plánovaný přebytek `E_surplus_after_t`
Pro sloty `t ∈ [t_detach, last_neg]`:
```text
E_surplus_after_t = Σ_t max(0,
pv_a_forecast[t] + pv_b_forecast[t]
- load_baseline[t]
- charge_to_battery_cap[t]
)
```
× `0,25 h` (případně jen část nad tím, co jde do `soc_need`).
**Použití:**
| Spotřebič | Pravidlo |
|-----------|----------|
| TČ předehřát v **T** | Jen pokud `E_surplus_after_t` > práh a **T** je dostatečně brzy |
| Bazén filtrace | Rozpočet hodin ≤ f(`E_surplus_after_t`), slunce) |
| Spirála (📋) | Až když TČ + bazén nestačí sežrat přebytek |
| Export B | Zbytek (zelený bonus) — lepší než -0,3 Kč/kWh, horší než vlastní spotřeba |
### 5.5 Chování PV A po T (📋)
**Ne** „tvrdě urazit A v 80 %“.
| Režim | LP / plán |
|-------|-----------|
| `t < t_detach` | Plné nabíjení z A+B směrem k `soc_need[t]` |
| `t ≥ t_detach` | **Necpát A do baterie** (`bc_pv` z A minimálně); A dostupné pro `pv_ld` / dům |
| Curtail A | Měkké nebo jen při riziku zbytečného exportu A za `sell < 0` |
**Deye:** reg **340** = forecast A curtail; při plném plánu bez exportu EMS 340 nemusí zapisovat (`plan_skips_deye_reg340_write`).
### 5.6 Výstupy do `solver_params` (📋)
Navrhované klíče v `planning_run.solver_params.inputs`:
| Klíč | Typ | Popis |
|------|-----|--------|
| `neg_sell_soc_ramp_wh` | pole | `soc_need[t]` per slot ISO |
| `t_detach_idx` | int | index slotu T |
| `e_surplus_after_t_wh` | float | integrál přebytku |
| `neg_sell_window_slots` | int | délka okna |
| `planner_build_tag` | string | např. `2026-05-28-neg-sell-b-ramp-v35` |
---
## 6. Termika — TČ, TUV, spirála
### 6.1 Co je dnes v solveru ✅
- Proměnná **`hp[t]`** 0…`rated_heating_power_w` v bilanci `load_site_expr`.
- **TUV look-ahead:** `tuv_usage_stats`, nouz pod `tuv_min_temp_c`, boost při poklesu pod `min+5 °C`.
- **Export TČ:** `heat_pump_enabled` / `heat_pump_setpoint_w` v `planning_interval`; Modbus zápis — viz [`control.md`](control.md) (často TODO).
**Není v modelu:** spirála, bojler jako samostatná zátěž, teplotní stav zásobníku jako spojitá proměnná v každém slotu (jen zjednodušený `tuv_pred`).
### 6.2 Pravidla podle typu dne (📋)
#### Den **se** `sell < 0`
| Kdy | TČ / TUV |
|-----|----------|
| Ranní pásma **před** `sell < 0` (pre-neg export) | **Netopit** (kromě nouze pod `tuv_min`) |
| Uvnitř `sell < 0`, `t < t_detach` | Minimum; priorita nabíjení bat |
| Uvnitř `sell < 0`, `t ≥ t_detach` | Komfort / předehřát dle `E_surplus_after_t` |
| Večer (sprcha) | **Doklep** na `tuv_comfort_temp_c` |
#### Den **bez** `sell < 0`
- TČ v slotech s **nízkým buy** a dobrým **COP** (poledne), ne v nejlepších **exportních** slotech FVE.
- Spirála: nízká priorita — preferovat prodej FVE.
### 6.3 TČ vs spirála (📋)
| Kritérium | Preferovat TČ | Preferovat spirálu |
|-----------|---------------|-------------------|
| Dlouhé `sell < 0`, B pokryje bat | Ano (COP) | Ne |
| Krátké okno, hodně FVE „na střeše“ | Částečně | Ano, pokud marginal cost ≈ 0 |
| Den bez `sell < 0` | Ano při dobrém COP | Spíš ne |
Spirála vyžaduje **novou zátěž** v DB + LP (`flex_load_spiral[t]` nebo signál Loxone).
### 6.4 Parametry termiky (rozhodnutí + otevřeno)
| Parametr | Stav | Hodnota / poznámka |
|----------|------|---------------------|
| `hp_no_run_pre_neg_export` | **Rozhodnuto** | `true` — v `pre_neg_pv_export_ts` **netopit** (raději export FVE). |
| `tuv_comfort_temp_c` | Otevřeno | Např. 5052 °C — doplnit do konfigurace site. |
| `tuv_preheat_temp_c` | Otevřeno | Např. 5558 °C — jen v bodu **T**, pokud `E_surplus_after_t` stačí. |
| `tuv_evening_topup_hour` | **Rozhodnuto** | **19:00** Europe/Prague — večerní doklep TUV (implementace v36). |
| Spirála | **Rozhodnuto** | Ovládání **Loxone**; model v EMS až v38. |
---
## 7. Bazén — filtrace a přitop (📋)
### 7.1 Provozní záměr (rozhodnutí home-01)
- **Filtrace ~1 kW** — min. **4 h/den**; **více hodin**, pokud `E_surplus_after_t` a přebytek dovolí (marginalní náklad ≈ 0).
- **Kdy:** přes den ve **slunečných** slotech (`is_daytime_pv_surplus_slot` nebo obdobné); **dynamicky** dle cen / přebytku, ne pevné okno 0917.
- **Proč ve dni:** cirkulace promíchá prohřátou hladinu.
- **Priorita:** po rampě bat / od bodu **T**, před exportem B za `sell < 0`.
- **Přitop vody:** **mimo** první verzi plánovače; začátek sezóny **ručně**; automatika později.
- **Exekuce:** **Shelly** — ovládání z EMS po implementaci assetu (v37).
### 7.2 Napojení na `E_surplus_after_t`
```text
pool_hours_max = min(
pool_filter_hours_per_day_config,
floor(E_surplus_after_t_wh / (1000 W × 0,25 h))
)
```
Rozložit do slotů s `sell < 0` ∧ slunce ∧ `t ≥ t_detach`.
### 7.3 Datový model (📋)
Zatím **není** v `db/migration`. Návrh:
- `ems.asset_pool` nebo rozšíření site config JSON
- sloupce: `filter_power_w`, `filter_hours_per_day`, `solar_window_start_hour`, `solar_window_end_hour` (Prague)
### 7.4 LP (📋)
- `pool_filter[t] ∈ [0, filter_power_w]`
- Zapnout jen pokud: `soc[t] ≥ soc_need[t]`, `sell[t] < 0`, slunce, zbývá denní rozpočet hodin
- Penalizovat `ge_pv` z B při plné baterii a zapnutém bazénu
---
## 8. EV
- Typicky **odpoledne** — session z telemetrie / `ev_session`.
- LP: deadline constraint na `target_soc` k `target_deadline`.
- Strategická vazba na v35: **po dosažení rampy** nebo v `allow_charge` + PV bohatých slotech — ne v ranním pre-neg exportu.
- Konflikt s večerním exportem bat řeší stávající masky `allow_discharge_export`.
---
## 9. UI plánování — význam čísel
Řádek v detailu slotu (**Planning.tsx**):
**„Škrcení A / ≈ reg 340“**
| Zobrazení | DB / výpočet | Význam |
|-----------|--------------|--------|
| **CURTAIL X W** | `pv_a_curtailed_w` | Kolik W z pole A plán **odebírá** (nechce využít). **0** = žádné škrcení. |
| **povoleno Y W** | `pv_a_forecast_solver_w pv_a_curtailed_w` | Odhad **reg 340** (*max solar power*) pro pole A. |
Příklad: forecast A = 4654 W, curtail = 1117 W → povoleno **3537 W**.
**Badge `sell prep` / `sell tail`:** z `solver_params.masks[].neg_sell_phase` (v32).
**Bat. / síť / SoC:** `battery_setpoint_w` / `grid_setpoint_w` / `battery_soc_target_pct` — po v34 u vysoké FVE **grid ≈ 0**, ne fiktivní import = load.
### 9.1 Vizualizace flexibilních zátěží — probrat před implementací (📋)
**Stav:** produktové rozhodnutí **není****neimplementovat** bazén / rozšířené TČ v UI ani v LP sinku, dokud není schválený návrh. Workshop mezi **v35** a **v37**.
**Proč:** flexibilní zátěže (TČ, bazén, spirála, EV) sdílí stejnou časovou osu jako energie (**T**, `E_surplus_after_t`, fáze sell&lt;0). Bez přehledného UI bude provoz těžko kontrolovatelný.
**Návrhy k diskusi** (nic z toho není závazná implementace):
| Nápad | Co ukázat |
|-------|-----------|
| **Pásma dne** | V grafu plánu: pre-neg export \| sell&lt;0 prep \| od **T** \| tail \| večerní export bat. |
| **Bod T** | Svislá značka + tooltip: `t_detach`, `e_surplus_after_t_wh`, odhad hodin bazénu. |
| **Rozpočet bazénu** | „Dnes 2/4 h filtrace naplánováno“ + zbývající Wh přebytku. |
| **Slot detail** | Kromě bat/síť/FVE: **TČ** (`heat_pump_setpoint_w`), **EV**, (budoucí) **bazén ON**, badge **flex sink**. |
| **Srovnání běhů** | Před/po v35: rampa SoC, méně fiktivního grid importu, curtail A. |
| **Živě vs plán** | Volitelně: telemetrie TUV / Shelly pool vs plánovaný stav (až bude data). |
**Výstup workshopu:** krátký mock / seznam widgetů v `Planning.tsx` + které sloupce ukládat do `planning_interval` / `solver_params`.
**Otevřené otázky UI:** viz [`docs/06-open-questions.md`](../06-open-questions.md).
---
## 10. Priorita flexibilních spotřebičů (📋)
Při `sell < 0` a plné / dostatečné baterii:
```text
1. Bazální dům (load-first, pv_ld)
2. Nouz TUV (tuv_min)
3. EV deadline
4. TČ komfort / doklep / předehřát (dle fáze)
5. Bazén filtrace (slunce, rozpočet hodin)
6. Spirála (až bude v EMS)
7. Export pole B (zelený bonus)
8. Curtail A (poslední ventil)
```
---
## 11. Roadmap implementace
| Pořadí | Fáze | Tag / doc | Obsah | Blokátor |
|--------|------|-----------|--------|----------|
| 1 | **v35** ✅ | `neg-sell-b-ramp-v35` | Rampa `soc_need` z B, **T**, `E_surplus_after_t`, uvolnění A | — |
| 2 | **UI workshop** | — | Vizualizace flex. zátěží — § 9.1; schválený návrh widgetů | **Před v37** |
| 3 | **v36** | `termika-v36` | Blok TČ pre-neg; TUV v `sell<0` po **T**; večerní doklep **19:00** Prague | v35 |
| 4 | **v37** | `pool-v37` | Bazén: Shelly, min 4 h/den, LP sink | UI workshop |
| 5 | **v38** | `spiral-v38` | Spirála (Loxone) + volba TČ vs spirála | v37 |
Každá implementační fáze: migrace (pokud DB), `planning_engine.py`, testy MILP, `planning-changelog.md`, ověření MCP na home-01.
---
## 12. Ověření v provozu
```sql
-- aktivní běh
select id, solver_params->>'planner_build_tag' as tag,
solver_params->'inputs'->>'pre_neg_pv_export_forecast_ok' as pre_neg_ok,
solver_params->'inputs'->>'t_detach_idx' as t_detach,
solver_params->'inputs'->>'e_surplus_after_t_wh' as e_surplus
from ems.planning_run
where site_id = (select id from ems.site where code = 'home-01')
and status = 'active'
order by created_at desc
limit 1;
-- sloty kolem poledne
select pi.interval_start at time zone 'Europe/Prague' as prague,
pi.battery_soc_target_pct,
pi.pv_a_curtailed_w,
pi.pv_a_forecast_solver_w,
pi.battery_setpoint_w,
pi.grid_setpoint_w,
pi.effective_sell_price
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 = 'home-01')
and pr.status = 'active'
and (pi.interval_start at time zone 'Europe/Prague')::date = current_date
order by pi.interval_start;
```
```bash
# testy
cd backend && python3 -m pytest tests/test_planning_dispatch_milp.py -k "NegSell or PreNeg or LoadFirst" -q
```
---
## 13. Související soubory
| Oblast | Cesta |
|--------|--------|
| Solver | `backend/services/planning_engine.py``_neg_sell_day_phases`, `_pre_neg_pv_export_*`, `solve_dispatch` |
| DB parametry | `db/migration/V083__planner_neg_sell_phases.sql` |
| Kontext site | `db/routines/R__039_fn_planning_site_context.sql` |
| FE plán | `frontend/src/pages/Planning.tsx``pvAAllowedW`, curtail badge |
| Deye 340 | `backend/services/control/setpoints.py``compute_pv_a_reg340_max_solar_w` |
| TUV stats | `ems.tuv_usage_stats`, `fn_update_tuv_usage_stats` |
---
## 14. Otevřená rozhodnutí
Živý seznam: [`docs/06-open-questions.md`](../06-open-questions.md) — sekce **Plánování — neg sell, termika, flexibilní zátěže**.
Zbývá hlavně: **čas večerního doklepu TUV** (~19h?), **návrh UI flex zátěží** (workshop před v37).