Problem
Predbat's current planner is a forward simulation that enumerates charge/discharge window combinations and picks the lowest-cost one. This works well for price-arbitrage on time-of-use tariffs, but it has a structural limitation for DC-coupled hybrid inverters: it cannot model the path where excess PV flows directly into the battery over the DC bus, bypassing the AC inverter limit.
This means that on flat or near-flat tariffs (small import/export spread), Predbat sees no price signal to pre-discharge — even when doing so would free battery headroom that lets DC-coupled PV charge the battery and export at full inverter capacity simultaneously, capturing generation that would otherwise clip.
Related issues that describe this symptom from different angles: #1206, #2472, #2341.
The DC-coupling gap
A DC-coupled hybrid inverter has two PV pathways:
- PV → AC (always available): limited by the AC inverter rating
- PV → Battery DC (only when battery is not full and inverter is in Self Consumption or Charge mode): bypasses the AC inverter limit
Predbat's model has no e_pvb variable. It sees PV as a single AC source capped at inverter_limit. So it never reasons about the trade-off:
"Pre-discharging now (earning 15 ¢/kWh) makes room for PV-DC charging tomorrow, which avoids importing at 16 ¢/kWh — worthwhile even on a near-flat tariff."
What a proper MILP would look like
A Mixed Integer Linear Program over a 48-hour horizon, 15-min steps, with variables:
| Variable |
Meaning |
e_imp[t], e_exp[t] |
Grid import / export (kWh) |
e_pvb[t] |
PV energy routed to battery via DC bus |
e_pv_ac[t] |
PV energy routed to AC inverter |
e_bdis[t] |
Battery discharge (kWh) |
soc[t] |
State of charge |
z_dis[t] ∈ {0,1} |
Mode binary: 0 = Self Consumption, 1 = Discharge/Export |
Key constraints:
soc[t+1] = soc[t] + η_ch·e_pvb[t] − e_bdis[t]/η_dis
e_imp + e_bdis + e_pv_ac = load + e_exp (AC balance)
e_bdis + e_pv_ac ≤ inv_limit (inverter AC cap)
e_pv_ac + η_inv·e_pvb ≤ pv·η_inv (PV DC split)
e_pvb ≤ bat_ch_max · (1 − z_dis) (DC only in SC mode)
e_exp ≤ inv_limit · z_dis (export only in Discharge)
Objective: minimise net grid cost over the horizon.
On a flat tariff, clipping has no direct cost in the objective, but the model correctly sees that pre-discharging now creates headroom for future PV-DC charging that avoids imports — enough to drive pre-discharge even with a 1 ¢/kWh spread.
Current workaround
We have a working standalone Predbat plugin implementing this MILP using HiGHS (highspy). It hooks on_update, solves the MILP, and calls select/select_option on a SolarEdge battery directly. It works, but it is invisible to Predbat's plan display and dashboard.
Proposed integration path
An optional replace_plan hook that Predbat calls instead of its own forward simulation when an external planner is registered. The hook would receive the same data Predbat already has (PV forecast, load forecast, SoC, rates, battery/inverter parameters) and return a list of charge/discharge windows that Predbat then executes and displays natively.
class MyMILPPlanner:
def plan(self, forecast: PredBatForecast) -> list[Window]:
...
Registering it via the plugin system:
def register_hooks(self, plugin_system):
plugin_system.register_planner(MyMILPPlanner())
This would give:
- Full Predbat dashboard / plan card visibility for MILP-derived schedules
- Predbat still handles all inverter service calls
- The DC-coupling model lives in the optional plugin, not in Predbat core
- Users on simple tariffs / AC-coupled systems are unaffected
Questions for maintainers
- Is a
replace_plan / register_planner hook something you'd consider merging, or is the architecture too deeply coupled to the forward simulation?
- Is there an existing internal boundary point (a method, a data class) where an external plan could be injected without a large refactor?
- Would you prefer this as a contribution to Predbat core, or as a documented external plugin pattern?
Happy to share the working plugin implementation and MILP formulation in detail if useful.
Problem
Predbat's current planner is a forward simulation that enumerates charge/discharge window combinations and picks the lowest-cost one. This works well for price-arbitrage on time-of-use tariffs, but it has a structural limitation for DC-coupled hybrid inverters: it cannot model the path where excess PV flows directly into the battery over the DC bus, bypassing the AC inverter limit.
This means that on flat or near-flat tariffs (small import/export spread), Predbat sees no price signal to pre-discharge — even when doing so would free battery headroom that lets DC-coupled PV charge the battery and export at full inverter capacity simultaneously, capturing generation that would otherwise clip.
Related issues that describe this symptom from different angles: #1206, #2472, #2341.
The DC-coupling gap
A DC-coupled hybrid inverter has two PV pathways:
Predbat's model has no
e_pvbvariable. It sees PV as a single AC source capped atinverter_limit. So it never reasons about the trade-off:What a proper MILP would look like
A Mixed Integer Linear Program over a 48-hour horizon, 15-min steps, with variables:
e_imp[t],e_exp[t]e_pvb[t]e_pv_ac[t]e_bdis[t]soc[t]z_dis[t]∈ {0,1}Key constraints:
Objective: minimise net grid cost over the horizon.
On a flat tariff, clipping has no direct cost in the objective, but the model correctly sees that pre-discharging now creates headroom for future PV-DC charging that avoids imports — enough to drive pre-discharge even with a 1 ¢/kWh spread.
Current workaround
We have a working standalone Predbat plugin implementing this MILP using HiGHS (
highspy). It hookson_update, solves the MILP, and callsselect/select_optionon a SolarEdge battery directly. It works, but it is invisible to Predbat's plan display and dashboard.Proposed integration path
An optional
replace_planhook that Predbat calls instead of its own forward simulation when an external planner is registered. The hook would receive the same data Predbat already has (PV forecast, load forecast, SoC, rates, battery/inverter parameters) and return a list of charge/discharge windows that Predbat then executes and displays natively.Registering it via the plugin system:
This would give:
Questions for maintainers
replace_plan/register_plannerhook something you'd consider merging, or is the architecture too deeply coupled to the forward simulation?Happy to share the working plugin implementation and MILP formulation in detail if useful.