diff --git a/backend/services/control/exporter_monolith.py b/backend/services/control/exporter_monolith.py index b4e24a3..5804f74 100644 --- a/backend/services/control/exporter_monolith.py +++ b/backend/services/control/exporter_monolith.py @@ -1290,6 +1290,20 @@ def _build_setpoints( forecast = int(pi.get("pv_a_forecast_solver_w") or 0) curtail = int(pi.get("pv_a_curtailed_w") or 0) pv_a_allowed = compute_pv_a_reg340_max_solar_w(int(pv_a_cap_w), forecast, curtail) + # Home-01 strategie: pokud jsou zároveň buy<0 i sell<0 a PV B vyrábí (necurtailable), + # chceme držet baterii „prázdnější“ pro PV B / další záporný nákup a PV A raději odstavit + # i když forecast PV A je nulový (predikce/telemetrie může být odpojená). + buy_raw = pi.get("effective_buy_price") + buy_f: float | None = float(buy_raw) if buy_raw is not None else None + pv_b = int(pi.get("pv_b_forecast_solver_w") or 0) + if ( + buy_f is not None + and sell_f is not None + and float(buy_f) < 0.0 + and float(sell_f) < 0.0 + and pv_b > 0 + ): + pv_a_allowed = 0 return ControlSetpoints( battery_w=int(pi["battery_setpoint_w"] or 0), grid_export_limit=abs(min(grid_sp, 0)), diff --git a/backend/tests/test_control_exporter_reg340.py b/backend/tests/test_control_exporter_reg340.py index d89fc1c..681f6cf 100644 --- a/backend/tests/test_control_exporter_reg340.py +++ b/backend/tests/test_control_exporter_reg340.py @@ -98,6 +98,22 @@ class BuildSetpointsReg340Tests(unittest.TestCase): assert sp is not None self.assertIsNone(sp.pv_a_allowed_w) + def test_neg_buy_and_sell_with_pv_b_forces_pv_a_off(self) -> None: + sp = _build_setpoints( + _auto_mode(), + _pi_base( + effective_buy_price=-3.0, + effective_sell_price=-2.0, + pv_b_forecast_solver_w=5000, + pv_a_forecast_solver_w=0, + pv_a_curtailed_w=0, + ), + pv_a_cap_w=3333, + inverter_manufacturer="Deye", + ) + assert sp is not None + self.assertEqual(sp.pv_a_allowed_w, 0) + class Reg340VerifyPolicyTests(unittest.TestCase): def test_reg340_not_critical_for_self_sustain(self) -> None: diff --git a/docs/04-modules/control.md b/docs/04-modules/control.md index 5b7948e..e053e09 100644 --- a/docs/04-modules/control.md +++ b/docs/04-modules/control.md @@ -136,6 +136,7 @@ bits 0–1). Detail registrů: [`modbus-registers.md`](modbus-registers.md) (reg - **Implementace:** `backend/services/control/exporter_monolith.py` — `export_setpoints` načte cap v `_load_inverter_config` (`ems.fn_inverter_pv_a_max_w(ai.id)`), `_build_setpoints` v režimu **AUTO** dopočítá `ControlSetpoints.pv_a_allowed_w`, `write_inverter_setpoints` zařadí **reg 340**, pokud je výrobce invertoru Deye, cap > 0 a `pv_a_allowed_w` je vyplněné. - **Data:** `pv_a_forecast_solver_w` / `pv_a_curtailed_w` z aktivního `planning_interval` (json z `ems.fn_planning_interval_at_offset`); cap = součet `nominal_power_wp` řiditelných polí na invertoru (bez nového sloupce v DB). +- **Home-01 policy (PV A off):** pokud jsou v aktuálním slotu zároveň `effective_buy_price < 0` a `effective_sell_price < 0` a zároveň `pv_b_forecast_solver_w > 0` (PV B vyrábí), exporter nastaví `pv_a_allowed_w = 0` (reg 340) i když je forecast PV A nulový — cílem je držet headroom v baterii pro PV B / další záporný nákup. - **Verify:** reg **340** není kritický → po 3× mismatch verify **bez** přepnutí do SELF_SUSTAIN (stejně jako reg 178); viz [`modbus-command-journal.md`](modbus-command-journal.md). #### Ověření po nasazení (smoke)