Files
ems/docs/04-modules/planning-neg-sell-strategy.md
Dusan Vojacek a53bcd0b81
Some checks failed
CI and deploy / migration-check (push) Failing after 14s
CI and deploy / deploy (push) Has been skipped
dokumentace planu
2026-05-26 13:00:33 +02:00

18 KiB
Raw Blame History

Strategie záporného výkupu, FVE A/B, termika a flexibilní zátěže (home-01)

Navazuje na planning.md, planning-arbitrage-accounting.md, planning-changelog.md, heat-pump.md, ev-charging.md.

Stav: část je implementovaná (v32v34), část je návrh (v35+ termika, bazén, spirála). 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).
Load-first (v34) Dům z pv_ld; při dostatečné FVE žádný fiktivní grid_import = load v plánu.
Reg 340 Deye max solar powerpv_a_forecast_solver_w pv_a_curtailed_w.

3. Časová osa dne (referenční home-01)

        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 (dnes v32, návrh v35):

Období v B Dnes (v32) Návrh (v35)
Začátek okna ASAP nabít na 80 % z A+B Nabít podle rampy SoC odvozené zpět z B od tail
Střed okna Měkký curtail A při SoC ≥ 80 % na začátku slotu Od T: A necpát do bat; B + přebytek
Tail (posledních N slotů) Rampa 80 % → 100 % Rampa z T / B → 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 (v32v34)

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 Plochý cíl SoC v prep fázi (% soc_max). 100 = legacy (tlak na max až v tail).
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.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.


5. Návrh v35 — energie: rampa z PV B, bod T, přebytek

📋 Není v produkci — specifikace pro implementaci.

5.1 Kotva vzadu (tail — beze změny konceptu)

Pro každý pražský den s sell < 0:

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

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:

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)

Definice: nejmenší t v prep části, kde:

soc_need[t] ≤ soc_detach_wh

kde soc_detach_wh může být:

  • konfigurovatelné % (náhrada za fixních 80 %), nebo
  • odvozené z soc_need v okamžiku přechodu („natural“ detach).

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

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 (č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 Konfigurovatelné teploty (📋 — rozhodnutí)

Parametr Navrh Poznámka
tuv_comfort_temp_c např. 5052 Denní komfort
tuv_preheat_temp_c např. 5558 V bodu T, podmíněně
tuv_evening_topup_before_min např. 90 Doklep před sprchou
hp_no_run_pre_neg_export true Blok TČ ve fázích A (v33 sloty)

7. Bazén — filtrace a přitop (📋)

7.1 Provozní záměr

  • Filtrace ~1 kW — regulovatelný denní rozpočet hodin (např. 46 h).
  • Kdy: jen ve slunečných hodinách (např. 09:0017:00 Prague, nebo příznak is_daytime_pv_surplus_slot z fn_load_planning_slots_full).
  • Proč ve dni: cirkulace promíchá prohřátou hladinu (uživatelský požadavek).
  • Priorita: po naplnění bat rampy / od T, před exportem B za sell < 0.

7.2 Napojení na E_surplus_after_t

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.


10. Priorita flexibilních spotřebičů (📋)

Při sell < 0 a plné / dostatečné baterii:

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

Fáze Tag / doc Obsah Závislost
v35 neg-sell-b-ramp-v35 Rampa soc_need z B, T, E_surplus_after_t, uvolnění A V083 sloupce; náhrada plochých 80 % v LP
v36 termika-v36 Blok TČ v pre-neg; TUV doklep; komfort v sell<0 po T v35
v37 pool-v37 Asset bazén, denní hodiny, LP sink v35
v38 spiral-v38 Spirála + volba TČ vs spirála Loxone/Modbus, v37

Každá fáze: migrace (pokud DB), planning_engine.py, testy MILP, zápis do planning-changelog.md, ověření na home-01 přes MCP.


12. Ověření v provozu

-- 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;
# 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.tsxpvAAllowedW, curtail badge
Deye 340 backend/services/control/setpoints.pycompute_pv_a_reg340_max_solar_w
TUV stats ems.tuv_usage_stats, fn_update_tuv_usage_stats

14. Otevřená rozhodnutí

Přesunuta do docs/06-open-questions.md sekce Plánování — neg sell, termika, bazén — nutné doplnit před v36+.