A battery is only as useful as the logic that controls it. This chapter covers how to decide — for each hour — whether to charge from solar, charge from the grid, discharge to the house, or hold. We progress from simple rule-based strategies to optimization-based approaches.
At each time step (typically each hour or 15-minute interval), the system must decide:
Inputs available:
The simplest and most common strategy. Logic at each time step:
solar = measured solar production (W)
load = measured household load (W)
soc = battery state of charge (0–1)
net = solar - load
if net > 0: # Solar surplus
if soc < soc_max:
charge_rate = min(net, max_charge_rate)
export = net - charge_rate
else:
export = net # Battery full, export all
else: # Deficit
deficit = -net
if soc > soc_min:
discharge_rate = min(deficit, max_discharge_rate)
grid_import = deficit - discharge_rate
else:
grid_import = deficit # Battery empty, import all
Performance: Very good for maximizing self-consumption. Does not optimize for tariff timing. Straightforward to implement in any home energy management system.
Goal: keep grid import below a threshold power level (e.g., 1 kW), regardless of price. Useful for households with demand charges.
grid_threshold = 1000 # W
if solar >= load:
# Charge battery from surplus
charge(solar - load)
grid_import = 0
else:
deficit = load - solar
if deficit > grid_threshold:
# Discharge battery to keep import below threshold
discharge_needed = deficit - grid_threshold
if soc > soc_min:
discharge(min(discharge_needed, available_capacity))
grid_import = min(deficit, grid_threshold)
Uses tariff schedule to decide when to charge from grid and when to discharge:
price_now = tariff.price(hour)
price_peak_threshold = 0.25 # €/kWh — discharge when more expensive
price_charge_threshold = 0.12 # €/kWh — charge from grid when cheaper
solar_surplus = max(0, solar - load)
deficit = max(0, load - solar)
# Charge from solar surplus (always)
if solar_surplus > 0 and soc < soc_max:
charge(solar_surplus)
# Discharge during peak price periods
elif deficit > 0 and price_now >= price_peak_threshold and soc > soc_min:
discharge(min(deficit, max_discharge_rate))
grid_import = deficit - actual_discharge
# Charge from grid during cheap periods (only if battery not already full)
elif price_now <= price_charge_threshold and soc < 0.5:
grid_charge(min(max_charge_rate, available_capacity_to_50pct))
else:
grid_import = deficit
Performance improvement vs. Strategy 1 on TOU tariffs: typically saves an additional 5–12% on the annual electricity bill.
The most powerful approach: use a day-ahead forecast of load and solar to compute the optimal battery schedule for the next 24 hours.
Formulated as a linear program:
Minimize: Σ_h [ grid_import(h) × price(h) ] − Σ_h [ grid_export(h) × export_price(h) ]
Subject to:
This is a convex LP, solvable in milliseconds with scipy.optimize.linprog or PuLP.
from dispatch_optimizer import optimize_day
schedule = optimize_day(
solar_forecast=solar_kwh_per_hour, # list of 24 values
load_forecast=load_kwh_per_hour,
prices=price_per_kwh,
battery_params={
"capacity_kwh": 10.0,
"dod": 0.90,
"charge_efficiency": 0.97,
"discharge_efficiency": 0.97,
"max_charge_kw": 5.0,
"max_discharge_kw": 5.0,
"initial_soc": 0.5,
}
)
print(f"Optimal daily cost: €{schedule['cost']:.2f}")
print(f"Grid imports: {sum(schedule['grid_import']):.1f} kWh")
Performance vs. Strategy 1: saves an additional 8–18% on the energy bill, depending on tariff variability and forecast accuracy.
A common practical optimization: if tomorrow morning’s weather forecast shows a cloudy day, charge the battery tonight from cheap off-peak grid power. If a sunny day is forecast, don’t bother — the battery will fill from solar.
Simple rule:
if tomorrow_solar_forecast < 50% of typical and price_now < off_peak_threshold:
charge to 80% SoC from grid
This captures much of the day-ahead optimization benefit without the LP complexity.
If backup during outages is a priority, always maintain a minimum SoC reserve:
backup_reserve = 0.30 # Keep 30% SoC as backup at all times
effective_soc_min = max(soc_min, backup_reserve)
For a 10 kWh battery with 30% backup reserve: 3 kWh always reserved, 7 kWh available for daily dispatch. This reduces self-consumption performance by ~5–10% but provides 6+ hours of critical-load backup.
Real home energy management systems (HEMS) implement these strategies and communicate with inverters, batteries, and smart meters using standard protocols:
| Protocol | Typical use |
|---|---|
| Modbus RTU/TCP | Inverter telemetry and control |
| SunSpec | Standardized solar/storage device interface |
| EEBus | European smart home energy protocol |
| OCPP | EV charging station communication |
| Home Assistant / OpenHAB | Open-source HEMS platforms |
| Proprietary APIs | SolarEdge, Fronius, Victron, etc. |
Open-source solutions like Home Assistant + Solcast integration can implement all the strategies in this chapter using automation rules, with accuracy improving as you add actual solar irradiance forecasts.
| Strategy | Complexity | Annual saving vs. no battery | Notes |
|---|---|---|---|
| No battery (solar only) | — | Baseline | ~50–55% SSR |
| Self-consumption priority | Low | +€250–350/yr | Most deployed |
| TOU-aware dispatch | Medium | +€310–440/yr | Requires tariff data |
| Day-ahead LP optimization | High | +€380–520/yr | Requires forecasts |
Figures assume: 5,000 kWh/year household, 4 kWp solar, 10 kWh battery, Lyon France, €0.25/kWh peak / €0.12/kWh off-peak TOU tariff.
Navigation: