Skip to content

fix: LOAD_SUPPORT discharge ceiling leaks real-time load spikes to grid despite ample SOE headroom #384

Description

@johanzander

Context

Split out from #381 after diagnosis. Frank-Leysen (Growatt MOD 5000TL3-XH via solax_modbus_growatt_min, TOU mode) reported small recurring overnight grid imports (0.1-0.2 kWh, a few tens of cents/night) while SOE had 3+ kWh of headroom above min_soe_kwh. Confirmed against his bess-debug-2026-07-21 / bess-debug-2026-07-22 bundles: every import happens in the same 15-minute period the battery is actively discharging under LOAD_SUPPORT, with the discharge-rate register at exactly the ceiling written for that period.

Root cause

_map_intent_to_rates (core/bess/inverter_controller.py:479-486) uses one shared branch for both LOAD_SUPPORT and BATTERY_EXPORT, capping the discharge-rate register at the DP's planned average power for the period:

elif intent in ("LOAD_SUPPORT", "BATTERY_EXPORT"):
    if battery_action_kw < -0.01:
        discharge_rate = self._scale_to_percent(abs(battery_action_kw), self.max_discharge_power_kw)
    else:
        discharge_rate = 0

These two intents are not physically equivalent:

  • BATTERY_EXPORTgrid_first: no deficit backstop. An open ceiling directly oversells stored energy beyond the arbitrage plan for the full period, regardless of load. Capping this is correct and should not change.
  • LOAD_SUPPORTload_first: self-limiting to the real house deficit (inverter_simulator.py:132-143, delivered_kwh = min(deficit, rate_kw*dt, ...)). A load spike above the written ceiling leaks straight to grid even with ample SOE and physical discharge capacity (max_discharge_power_kw) sitting unused, because the register — not the deficit — is the binding constraint.

Note: LOAD_SUPPORT is deliberately not hardcoded to 100% — that was the bug fixed in #147 (battery drained early on high-consumption days, blowing past reserve planned for a later, pricier period). So the fix is not "remove the cap," it's making the cap economically aware.

Proposed fix

Add a shadow-price-gated discharge ceiling for LOAD_SUPPORT, mirroring the existing SOLAR_EXPORT shadow-price gate (docs/agents/bess-knowledge.md "Governing Economic Law" section, battery_system_manager.py solar_export_discharge_rate()): allow the discharge ceiling to open above the plan-scaled value specifically when the DP's own shadow price for that period indicates the marginal stored kWh is not earmarked for a later, more valuable use (i.e., genuine surplus headroom exists). Keep BATTERY_EXPORT's ceiling exactly as-is (plan-scaled, no gate) since it lacks the physical backstop that makes this safe for LOAD_SUPPORT.

Acceptance criteria

  • LOAD_SUPPORT discharge ceiling is no longer a single shared branch with BATTERY_EXPORT.
  • When shadow price indicates no future need for reserve, LOAD_SUPPORT can open beyond the plan-scaled ceiling to cover a real-time load deficit up to max_discharge_power_kw.
  • When shadow price indicates the reserve is needed for a later period, current plan-scaled behavior is preserved (no Battery discharge cannot be paced for home support (LOAD_SUPPORT dumps greedily on deep evening peaks) #147 regression).
  • Regression test replaying Frank's bess-debug-2026-07-21/-22 bundles shows the previously-leaked overnight import periods now served from battery instead of grid, with no change to daytime/evening schedule.
  • BATTERY_EXPORT behavior is unchanged.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions