implmemtace cuttoff genportu
Some checks failed
CI and deploy / migration-check (push) Failing after 9s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-04-20 10:41:10 +02:00
parent d8dbb284fd
commit b8515f30df
15 changed files with 265 additions and 5 deletions

View File

@@ -187,6 +187,9 @@ class DispatchResult:
#: Explicitní fyzický režim Deye pro control exporter (PASSIVE / SELL / CHARGE).
#: Cíl: odstranit heuristiky z exporteru a nést záměr přímo v plánu.
deye_physical_mode: str
#: True = v daném slotu odpojit GEN port (MI export cutoff) přes reg 179 bits01.
#: None = lokalita tuto funkci nemá / nepoužívá.
deye_gen_cutoff_enabled: bool | None
ev1_setpoint_w: Optional[int]
ev2_setpoint_w: Optional[int]
ev1_via_bat_w: int
@@ -346,6 +349,14 @@ def solve_dispatch(
hp = [pulp.LpVariable(f"hp_{t}", 0, heat_pump.rated_heating_power_w) for t in range(T)]
soc_deficit_24h = pulp.LpVariable("soc_deficit_24h", 0, battery.usable_capacity_wh)
# GEN port cut-off (BA81): binární proměnná pouze pokud je feature povolená v konfiguraci site/invertoru.
gen_cutoff_enabled = bool(getattr(grid, "deye_gen_microinverter_cutoff_enabled", False))
z_gen_cutoff = (
[pulp.LpVariable(f"z_gen_cutoff_{t}", cat=pulp.LpBinary) for t in range(T)]
if gen_cutoff_enabled
else None
)
# EV proměnné per vozidlo
ev_direct = [[pulp.LpVariable(f"evd_{e}_{t}", 0,
min(vehicles[e].max_charge_power_w, grid.max_import_power_w))
@@ -391,8 +402,13 @@ def solve_dispatch(
ev_total_t = pulp.lpSum(ev_direct[e][t] + ev_via_bat[e][t] for e in range(EV))
# Energetická bilance
pv_b_effective = (
float(s.pv_b_forecast_w) * (1 - z_gen_cutoff[t])
if z_gen_cutoff is not None
else float(s.pv_b_forecast_w)
)
prob += (
pv_a_net + s.pv_b_forecast_w + gi[t] + bd[t]
pv_a_net + pv_b_effective + gi[t] + bd[t]
== s.load_baseline_w + ev_total_t + hp[t] + bc[t] + ge[t]
)
@@ -410,6 +426,9 @@ def solve_dispatch(
# Záporná prodejní cena → zakázat export
if s.sell_price < 0:
prob += ge[t] == 0
# GEN cut-off používáme jen jako nástroj pro BLOCK_EXPORT (sell < 0).
if z_gen_cutoff is not None and s.sell_price >= 0:
prob += z_gen_cutoff[t] == 0
# Záporná nákupní cena → cap import (baseline domu + akumulace + řízené zátěže)
if s.buy_price < 0:
@@ -552,6 +571,10 @@ def solve_dispatch(
elif batt_w > 0 and grid_w > 0:
deye_mode = "CHARGE"
deye_gen_cutoff = None
if z_gen_cutoff is not None:
deye_gen_cutoff = bool(round(float(pulp.value(z_gen_cutoff[t]) or 0)))
cost = (
pulp.value(gi[t]) * slots[t].buy_price * INTERVAL_H / 1000
- pulp.value(ge[t]) * slots[t].sell_price * INTERVAL_H / 1000
@@ -563,6 +586,7 @@ def solve_dispatch(
battery_soc_target = soc_pct,
grid_setpoint_w = grid_w,
deye_physical_mode = deye_mode,
deye_gen_cutoff_enabled = deye_gen_cutoff,
ev1_setpoint_w = round(pulp.value(ev_direct[0][t]) + pulp.value(ev_via_bat[0][t]))
if slots[t].ev1_connected else None,
ev2_setpoint_w = round(pulp.value(ev_direct[1][t]) + pulp.value(ev_via_bat[1][t]))
@@ -847,6 +871,7 @@ async def _load_site_context(site_id: int, db):
grid = SimpleNamespace(
max_import_power_w=int(g["max_import_power_w"]),
max_export_power_w=int(g["max_export_power_w"]),
deye_gen_microinverter_cutoff_enabled=bool(g.get("deye_gen_microinverter_cutoff_enabled") or False),
)
vehicles: list[SimpleNamespace] = []
@@ -995,6 +1020,7 @@ async def _save_planning_run(
"battery_soc_target_pct": r.battery_soc_target,
"grid_setpoint_w": r.grid_setpoint_w,
"deye_physical_mode": r.deye_physical_mode,
"deye_gen_cutoff_enabled": r.deye_gen_cutoff_enabled,
"ev1_setpoint_w": r.ev1_setpoint_w,
"ev2_setpoint_w": r.ev2_setpoint_w,
"ev1_via_bat_w": r.ev1_via_bat_w,