HOTFIX BA81: export plan guard neodstavuje pole B při kladné vykupní ceně
Some checks failed
CI and deploy / deploy (push) Has been cancelled
CI and deploy / migration-check (push) Has been cancelled

_apply_export_plan_guard při export_mode=NONE (plán nabíjí baterii, neexportuje)
vynucoval _passive_no_export_guard s export_ban=True + deye_gen_cutoff_enabled=True
bez ohledu na cenu -> reg 178 bity 0-1=3 (MI cutoff) + reg 145=0 a mikroinvertory
(pole B) fyzicky stály i při sell +1.36 Kč (BA81 dnes: gen port ~0 W od 12:16Z,
SoC 64 %, stringy 4.2 kW do baterie). Tvrdý ban nově JEN při záporné vykupní;
při kladné guard dál drží PASSIVE/143=0/baterie nevybíjí do sítě, ale MI jedou
(absorbce do baterie, přetok se prodá). Plánový z_gen_cutoff se respektuje.

Pre-existing fail test_neg_buy_and_sell_with_pv_b_forces_pv_a_off padá i na main
(pv_a_allowed_w None != 0) — nesouvisí, řešit zvlášť.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dusan Vojacek
2026-06-12 15:07:17 +02:00
parent a208cc627d
commit c7f595c587
3 changed files with 42 additions and 7 deletions

View File

@@ -231,8 +231,18 @@ def _build_setpoints(
return None
def _passive_no_export_guard(sp: ControlSetpoints) -> ControlSetpoints:
"""PASSIVE, žádný vývoz do sítě; vybíjení baterie do sítě vynulováno (reg 109 přes export_ban)."""
def _passive_no_export_guard(
sp: ControlSetpoints, *, hard_ban: bool = True
) -> ControlSetpoints:
"""
PASSIVE, žádný vývoz do sítě z plánu (143=0, grid_setpoint>=0, baterie nevybíjí do sítě).
``hard_ban=True`` (záporná vykupní): navíc export_ban (145=0) a MI cut-off na GEN
portu (reg 178) — přebytek pole B NESMÍ do sítě.
``hard_ban=False`` (kladná vykupní, plán jen nechce exportovat baterii/stringy):
mikroinvertory NEodstavovat — jejich výroba se absorbuje do baterie/zátěže a
případný fyzický přetok se při kladné ceně prodá (cut-off by výrobu zahodil).
"""
bat = int(sp.battery_w or 0)
if bat < 0:
bat = 0
@@ -248,8 +258,8 @@ def _passive_no_export_guard(sp: ControlSetpoints) -> ControlSetpoints:
target_soc_pct=sp.target_soc_pct,
deye_physical_mode="PASSIVE",
export_mode="NONE",
export_ban=True,
deye_gen_cutoff_enabled=True,
export_ban=bool(sp.export_ban) or hard_ban,
deye_gen_cutoff_enabled=bool(sp.deye_gen_cutoff_enabled) or hard_ban,
effective_sell_price_czk_kwh=sp.effective_sell_price_czk_kwh,
pv_a_allowed_w=sp.pv_a_allowed_w,
lock_battery=sp.lock_battery,
@@ -293,7 +303,9 @@ def _apply_export_plan_guard(
site_id,
reason,
)
return _passive_no_export_guard(sp)
# MI cut-off / 145=0 jen při záporné vykupní; export_mode NONE s kladnou cenou
# nesmí odstavit pole B (BA81 2026-06-12: cutoff při sell +1.36 → výroba MI zahozena).
return _passive_no_export_guard(sp, hard_ban=neg_sell)
def _apply_price_failsafe_guard(

View File

@@ -73,7 +73,30 @@ class ExportPlanGuardTests(unittest.TestCase):
out = _apply_export_plan_guard(1, _auto_mode(), pi, sp)
self.assertEqual(get_deye_mode(out), "PASSIVE")
self.assertEqual(out.battery_w, 0)
self.assertTrue(out.export_ban)
self.assertEqual(out.grid_export_limit, 0)
# Kladná vykupní: žádný tvrdý ban — MI (pole B) se NEodstavuje, 145 zůstává 1
# (BA81 2026-06-12: cutoff při sell +1.36 zahazoval výrobu mikroinvertorů).
self.assertFalse(out.export_ban)
self.assertFalse(out.deye_gen_cutoff_enabled)
def test_export_mode_none_positive_sell_respects_plan_cutoff(self) -> None:
# Plán explicitně chce cut-off (z_gen_cutoff) -> guard ho nesmí shodit.
sp = _sp(
grid_setpoint_w=0,
battery_w=2000,
export_mode="NONE",
deye_physical_mode="PASSIVE",
deye_gen_cutoff_enabled=True,
)
pi = _DictRecord(
{
"grid_setpoint_w": 0,
"effective_sell_price": 2.5,
"export_mode": "NONE",
}
)
out = _apply_export_plan_guard(1, _auto_mode(), pi, sp)
self.assertTrue(out.deye_gen_cutoff_enabled)
def test_profitable_export_unchanged(self) -> None:
sp = _sp()

View File

@@ -64,7 +64,7 @@ Po `_build_setpoints`, před zápisem Modbus (`orchestrator.export_setpoints`):
| Guard | Podmínka | Efekt |
|-------|----------|--------|
| **`_apply_export_plan_guard`** | `effective_sell_price < 0` **nebo** (`export_mode = NONE` a `grid_setpoint_w ≥ 0`) | PASSIVE, `export_ban`, `grid_export_limit = 0`, vybíjení baterie do sítě vynulováno (`battery_w = max(0, …)`), `deye_physical_mode = PASSIVE` |
| **`_apply_export_plan_guard`** | `effective_sell_price < 0` **nebo** (`export_mode = NONE` a `grid_setpoint_w ≥ 0`) | PASSIVE, `grid_export_limit = 0`, vybíjení baterie do sítě vynulováno (`battery_w = max(0, …)`). **Tvrdý ban** (`export_ban` → reg 145 = 0 + MI cut-off reg 178) **jen při záporné vykupní**`export_mode = NONE` s kladnou cenou pole B neodstavuje (mikroinvertory se absorbují do baterie/zátěže, případný přetok se prodá; BA81 2026-06-12 cutoff při sell +1.36 zahazoval výrobu) |
| **`_apply_price_failsafe_guard`** | `is_predicted_price = true` | PASSIVE, všechny výkonové setpointy 0, žádný export |
Implementace: `backend/services/control/setpoints.py`. Ověření: `pytest backend/tests/test_control_export_plan_guard.py`.