14 KiB
name, overview, todos, isProject
| name | overview | todos | isProject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| planner-battery-tuning | Opravíme nesoulad mezi plánem a zápisem do Deye při nabíjení z FVE přebytku, doplníme SQL-first vstupy pro denní safety charge, aplikujeme je v LP jako soft penalty a uložíme debug snapshot každého běhu planneru. |
|
false |
Stabilizace plánovače baterie
Cíl
Opravit tři související problémy:
- Plán někdy chce nabíjet baterii z PV přebytku, ale Deye dostane
reg108 = 0, takže fyzicky nenabíjí. - Rolling replan umí posouvat plánované nabíjení dál a dál, až levné PV okno uteče.
- Malé baterie BA81/KV1 potřebují robustní denní nabití pro noc, ale zároveň nesmí ztratit schopnost ekonomicky cyklovat a prodávat v opravdu drahých sell oknech.
Datové zjištění
BA81= site3,KV1= site4,home-01= site2.- KV1 run
8101pro slot 17:15 plánovalbattery_setpoint_w = 4737W,grid_setpoint_w = -13W,deye_physical_mode = PASSIVE;modbus_commandnásledně zapsal a ověřil Deyeregister = 108,value_to_write = 0. To je konkrétní bug v control exportu. - BA81 historie rolling runů ukazuje posun prvního charge slotu s časem. To je částečně normální receding-horizon efekt, ale nesmí prodat levný PV přebytek, který je potřeba pro pozdější sell peak nebo noční baseload.
planner_terminal_soc_value_factornení jediné řešení. BA81/KV1 mají0.2, home-01 má0.9; nezvyšovat BA81/KV1 plošně na0.9, protože to může vrátit starou neochotu malé baterie cyklovat.
Architektonické rozhodnutí
- SQL-first zůstává: výpočet vstupů pro planner patří do SQL funkcí / view.
- Safety charge nesmí být hard
allow_chargemaska. SQL má spočítat vstupní hodnoty, LP je použije jako soft penalty v objective. - Debug snapshot ukládat do existujícího
ems.planning_run.solver_params. Samostatnou tabulku nezavádět v první iteraci. - Hodnota energie v baterii není jedna konstanta:
battery_value = max(future_avoided_buy, future_sell_opportunity) - degradation, plus samostatný měkký noční buffer.
Implementace
1. Oprava Deye exportéru
Soubory:
Požadované chování:
- Pokud
ControlSetpoints.battery_w > 0, Deye musí dostat nenulový nabíjecí proud podlebattery_w, i kdyžgrid_setpoint_w < 0. - V tomto scénáři zůstává
deye_physical_mode = PASSIVE, pokud plán explicitně neurčíCHARGE. Nejde o grid-charge režim; jde o nabíjení z PV přebytku a současný export zbytku. discharge_av tomto scénáři nastavit na0nebo jinak omezit tak, aby Deye současně nevybíjel baterii.- Existující SELL a PRESERVE chování neměnit.
Konkrétní místo:
- V
write_inverter_setpoints()je problém v PASSIVE větvi, která přes_deye_zero_export_amps_for_passive()vracícharge_a = 0, kdyžgrid_w < 0abat_w >= 0. - Přidej před tuto větev explicitní případ
bat_w > 0:charge_a = battery_watts_to_amps(bat_w, inv.max_charge_a),discharge_a = 0.
2. SQL vstupy pro daytime safety charge
Soubory:
db/routines/R__063_fn_load_planning_slots_full.sql- případně nová repeatable funkce v
db/routines
Neimplementovat jako hard masku. Nezakazovat / nepovolovat sloty natvrdo jen kvůli safety charge.
Doplnit SQL výstupy, které Python LP použije:
night_baseload_target_wh: kolik Wh je potřeba od večera do dalšího ranního PV okna.night_baseload_buffer_wh: bezpečnostní přirážka, např. procento z cíle.safety_soc_target_wh: doporučený SoC cíl pro slot.future_avoided_buy_czk_kwh: odhad ceny, kterou baterie ušetří, pokud energii necháme pro vlastní spotřebu.future_sell_opportunity_czk_kwh: nejlepší relevantní budoucí sell příležitost v horizontu.is_daytime_pv_surplus_slot: pomocný boolean pro debug a vážení cíle.
Preferovaný způsob:
- Rozšířit
ems.fn_load_planning_slots_full(...), protože už je hlavní zdroj slotových vstupů pro_load_slots(). - Pokud by rozšíření funkce bylo příliš velké, vytvořit samostatnou
ems.fn_planning_safety_charge_inputs(site_id, from, to, current_soc_wh)a joinovat podleinterval_startv SQL/Pythonu.
Výpočet nočního okna:
- Praktická první verze: noc = od lokálního západu / večerního konce PV surplus do dalšího rána, zjednodušeně
20:00-06:00 Europe/Prague. - Přesnější verze později: od posledního dnešního slotu s významným PV forecastem do prvního zítřejšího slotu s významným PV forecastem.
- Pro první implementaci stačí konzervativní a čitelná definice, hlavně ji uložit do debug snapshotu.
3. Rozšíření Python datových tříd a načítání slotů
Soubor:
Upravit PlanningSlot:
- Přidat volitelná pole pro SQL safety vstupy:
safety_soc_target_wh: float | Nonenight_baseload_target_wh: float | Nonenight_baseload_buffer_wh: float | Nonefuture_avoided_buy_czk_kwh: float | Nonefuture_sell_opportunity_czk_kwh: float | Noneis_daytime_pv_surplus_slot: bool = False
Upravit _load_slots():
- Načíst nové sloupce ze SQL.
- Pokud SQL sloupce dočasně nejsou k dispozici, použít bezpečný fallback
None/False, aby testy starších DB funkcí nespadly. - Nepočítat noční baseload ad-hoc v Pythonu, pokud už SQL funkce hodnotu vrací.
4. LP objective: soft safety target
Soubor:
Přidat do solve_dispatch():
- Pro každý slot
tssafety_soc_target_wh is not Nonevytvořit spojitou proměnnousafety_deficit_wh[t] >= 0. - Přidat omezení:
safety_deficit_wh[t] >= safety_soc_target_wh[t] - soc[t]
- Přidat do objective penalizaci:
safety_deficit_wh[t] * safety_penalty_czk_per_wh[t]
Výpočet penalty:
battery_value_czk_kwh = max(future_avoided_buy_czk_kwh, future_sell_opportunity_czk_kwh) - degradation_cost_effectivesafety_penalty_czk_per_wh = max(0, battery_value_czk_kwh) / 1000- Přidat rozumný clamp, aby penalty nebyla extrémní kvůli vadné ceně.
Chování:
- Pokud je vysoký sell peak ekonomicky lepší než držet energii pro noc, LP smí target porušit a prodat.
- Pokud je budoucí nákup drahý, typicky KV1, deficit bude drahý a LP bude energii spíš držet pro vlastní spotřebu.
- Toto není hard constraint.
5. Near-term commitment proti deferralu
Soubory:
backend/services/planning_engine.py- DB čtení z
ems.planning_run/ems.planning_intervalpřes SQL funkci nebo jednoduchý read model
Cíl:
- Rolling replan nesmí bez náhrady odsunout nejbližší plánované nabíjení z PV přebytku, pokud předchozí aktivní plán pro stejný nebo nejbližší slot chtěl nabíjet.
První jednoduchá implementace:
- Při rolling replanu načíst předchozí aktivní plán pro stejné
site_id. - Najít nejbližší 1-2 sloty od
replan_from, kde předchozí plán měl:battery_setpoint_w > 500pv_a_forecast_solver_w + pv_b_forecast_solver_w > load_baseline_w- ideálně
grid_setpoint_w <= 0
- V novém LP pro odpovídající slot přidat soft proměnnou
charge_commitment_shortfall_w[t] >= previous_battery_charge_w - bc[t]. - Penalizace má být malá, ale nenulová: má zabránit bezdůvodnému odsunu, ne přebít skutečně lepší ekonomiku.
- Uložit do debug snapshotu, kdy commitment vznikl a kolik stál.
Neimplementovat jako hard constraint.
6. Debug snapshot do solver_params
Soubory:
Upravit _save_planning_run():
- Rozšířit
run_metaosolver_params. solver_paramsbude JSON serializovatelný dict.
Upravit ems.fn_planning_run_commit(...):
- Při insertu do
ems.planning_runuložitsolver_params = p_run_meta->'solver_params'.
Minimální struktura JSON:
{
"version": 1,
"inputs": {
"current_soc_wh": 0,
"operating_mode": "AUTO",
"battery": {
"usable_capacity_wh": 0,
"min_soc_wh": 0,
"reserve_soc_wh": 0,
"degradation_cost_czk_kwh": 0,
"planner_terminal_soc_value_factor": 0.2
}
},
"masks": [
{
"slot": "2026-05-04T15:45:00+00:00",
"allow_charge": true,
"allow_discharge_export": false
}
],
"soc_bounds": [
{
"slot": "2026-05-04T15:45:00+00:00",
"soc_min_wh": 0,
"arb_floor_wh": 0,
"soc_panel_min_wh": 0,
"safety_soc_target_wh": 0
}
],
"objective_terms": [
{
"slot": "2026-05-04T15:45:00+00:00",
"buy_price": 0,
"sell_price": 0,
"future_avoided_buy_czk_kwh": 0,
"future_sell_opportunity_czk_kwh": 0,
"battery_value_czk_kwh": 0,
"safety_deficit_penalty_czk_per_wh": 0,
"commitment_penalty_czk_per_w": 0
}
],
"chosen_slots": {
"charge_commitment": [],
"high_sell_windows": [],
"night_window": {
"start": "2026-05-04T18:00:00+00:00",
"end": "2026-05-05T04:00:00+00:00",
"target_wh": 0
}
}
}
7. Debug read model
Soubor:
- nová repeatable funkce v
db/routines, např.R__086_fn_planning_run_debug.sql
Vytvořit ems.fn_planning_run_debug(p_run_id int):
- Vrátí jeden
jsonb. - Obsahuje:
- metadata z
planning_run, solver_params,- intervaly z
planning_intervalpro daný run, - krátký souhrn: první charge slot, první battery export slot, nejdražší sell sloty, největší safety deficit.
- metadata z
Použití přes MCP:
select ems.fn_planning_run_debug(8107);
8. Parametry
Nepřepisovat plošně BA81/KV1 na planner_terminal_soc_value_factor = 0.9.
Nové parametry preferovaně v ems.asset_battery přes novou migraci:
planner_daytime_charge_target_enabled boolean default trueplanner_night_baseload_buffer_percent numeric default 20planner_daytime_charge_price_quantile numeric default 0.70planner_charge_commitment_penalty_czk_kwh numeric default 0.20
Pokud je rozsah příliš velký, první iterace může mít konzervativní konstanty v Pythonu, ale plánovaná cílová podoba je DB parametrizace.
9. Testy
Najít existující testovací styl v repu a přidat testy co nejblíže dotčeným modulům.
Povinné scénáře:
- Control exporter:
battery_w > 0,grid_setpoint_w < 0,deye_physical_mode = PASSIVEvede nareg108 > 0,reg109 = 0. - Control exporter: SELL režim se nezmění.
- Planner safety: malá baterie, PV surplus přes den, noční baseload, pozdější drahý sell slot. LP má nabíjet v rozumně levném PV slotu a neodsunout charge donekonečna.
- Planner economics: pokud
sell_nowpřevyšuje budoucí avoided buy plus degradaci, LP smí porušit safety target a prodat. - Planner economics KV1-like: pokud budoucí buy je drahý a sell není dost vysoký, LP má držet energii pro vlastní spotřebu.
10. Dokumentace
Aktualizovat:
Dokumentace musí popsat:
- rozdíl mezi plánem, Deye fyzickým režimem a registry
108/109, - PV-surplus charging při současném exportu,
solver_paramsdebug snapshot afn_planning_run_debug,- rozdíl mezi hard maskami (
allow_charge,allow_discharge_export) a soft LP penalizacemi, - že
planner_terminal_soc_value_factornení jediný mechanismus ochrany malé baterie.
Ověření
- Spustit backend testy pro control a planner.
- Spustit Flyway validate lokálně.
- Přes MCP ověřit po nasazení:
- pro BA81/KV1 sloty s
battery_setpoint_w > 0agrid_setpoint_w < 0má následnýmodbus_command.register = 108hodnotu > 0, planning_run.solver_paramsneníNULLa obsahujeinputs,masks,soc_bounds,objective_terms,chosen_slots,select ems.fn_planning_run_debug(<run_id>)vrací vysvětlitelný JSON,- rolling replan neodkládá nabíjení z levného PV přebytku bez viditelného ekonomického důvodu v debug snapshotu.
- pro BA81/KV1 sloty s