AI Solar Panel
§5 Section 5 of 6 2,934 words · 13 min

Battery and Tariff Optimisation

Most writing about solar battery Octopus Agile optimisation stops at “charge when it’s cheap, discharge at the peak.” That advice is worth maybe £200 a year. The remaining £150 to £400 sits in the decisions that rule doesn’t cover: how full to charge given tomorrow’s forecast, whether the spread you’re chasing actually clears your battery’s wear cost, which export tariff changes the answer, and whether your simulation is telling you the truth or quietly double-counting efficiency losses.

This page is the spine for that work. It assumes you can pull a CSV, write or at least read Python, and that you have either a battery already or a firm plan to buy one. If you’re still deciding on capacity, start with sizing a home battery from your own load curve, because almost every number below scales with usable kWh.

Define the objective before you optimise anything

“Save money” is not an objective function. Write it down properly, because the version you pick changes the plan the solver produces.

The usable version for most households is: minimise sum(import_kWh × import_price) − sum(export_kWh × export_price) + battery_wear_cost + standing_charge over a rolling 48-hour horizon, subject to inverter power limits, battery capacity, and a state-of-charge floor.

Three parts of that get dropped by accident. Standing charge is the most common: at 61p/day that’s £223 a year, and Agile carries a different standing charge from Go in several regions, so a unit-rate-only comparison can hand you the wrong tariff. Export revenue is the second, and it’s the one that flips decisions rather than just shifting totals. Battery wear is the third, and it deserves its own section.

Your cost per stored kWh is the number that governs everything

Take the installed cost of the battery, divide by the energy it will deliver over its warranted life. A 9.5 kWh usable pack at £4,000 installed, warranted for 6,000 cycles, with 88% AC-to-AC round-trip efficiency:

6,000 cycles × 9.5 kWh × 0.88 = 50,160 kWh delivered
£4,000 ÷ 50,160 kWh = 7.97p per delivered kWh

Round it to 8p. That is your floor. Any arbitrage where the spread between the effective charge cost and the avoided import price is under 8p/kWh is you converting battery life into nothing. It also means the effective cost of a kWh you stored overnight at 9p is not 9p, it’s 9 ÷ 0.88 + 8 = 18.2p. Against a 33p peak that’s still a strong trade. Against a 24p shoulder rate it’s thin, and against a 19p overnight-adjacent rate it’s a loss.

Predbat exposes this directly as metric_battery_cycle, in pence per kWh cycled. The default is low or zero. Set it to your own figure and the plans it generates get noticeably less greedy about shallow arbitrage, which is usually correct.

Recompute this whenever your capacity assumptions change. If you’re comparing a 5 kWh pack against a 13 kWh one, the per-kWh wear cost moves, the cycle depth moves, and so does the set of trades worth taking; the load-curve sizing model is where that comparison belongs.

Getting your own half-hourly data out

Octopus’s REST API is the fastest route and needs no scraping. Your API key goes in as the HTTP Basic username with a blank password.

GET https://api.octopus.energy/v1/electricity-meter-points/{mpan}/meters/{serial}/consumption/
    ?period_from=2025-10-01T00:00Z&period_to=2026-10-01T00:00Z&page_size=25000&order_by=period

Rates come from the product endpoint, with your region letter on the end of the tariff code (C for London, H for Southern, J for South Eastern, M for Yorkshire):

GET https://api.octopus.energy/v1/products/AGILE-24-10-01/electricity-tariffs/
    E-1R-AGILE-24-10-01-J/standard-unit-rates/?period_from=2026-02-11T00:00Z&period_to=2026-02-12T00:00Z
{
  "count": 48,
  "results": [
    {"value_exc_vat": 39.24, "value_inc_vat": 41.20,
     "valid_from": "2026-02-11T17:30:00Z", "valid_to": "2026-02-11T18:00:00Z"},
    {"value_exc_vat": 36.80, "value_inc_vat": 38.64,
     "valid_from": "2026-02-11T17:00:00Z", "valid_to": "2026-02-11T17:30:00Z"}
  ]
}

Two traps live in there. Rates return newest-first unless you sort them, so a naive zip() against your ascending consumption series silently reverses your whole day. And the consumption timestamps carry a local offset, which means the clock-change days have 46 and 50 half-hours; resampling with a naive datetime index will drop or duplicate an hour and you’ll spend an evening hunting a phantom £3 discrepancy in March.

If you’re not with Octopus, the Hildebrand Glow CAD gives you a local MQTT feed at roughly 10-second resolution plus half-hourly totals, and n3rgy’s data service pulls the same DCC records your supplier sees. Inverter-side data (GivEnergy’s API, GivTCP over local Modbus, Solis or Sunsynk portal exports, the Fox ESS cloud) is what you need for PV and battery internals, because your import meter cannot see any of it.

That last point matters more than it sounds. If you already own a battery and you’re modelling a bigger one, your metered import is net of the existing battery and PV. You need gross house load, which is import + PV_self_consumed + battery_discharge − battery_charge_from_grid. Get it from the inverter, not the meter, or your new model will be built on a demand curve that doesn’t exist.

Reading Agile for its shape, not its average

Agile’s unit rate is derived from the N2EX day-ahead auction with a regional multiplier and a fixed adder, and a steeper multiplier applies to the 16:00 to 19:00 block. Rather than trusting any published coefficients, fit them yourself: regress a year of Agile rates against the corresponding day-ahead hourly prices, split into peak and non-peak regimes. You’ll get two clean lines, a cap at the top, and a visible discontinuity where the peak formula kicks in. Fifteen lines of numpy, and afterwards you can price hypothetical wholesale scenarios without waiting for Octopus to publish.

What you’re actually buying on Agile is variance. The annual mean rate is often within 2p of a flat tariff. The money is in the daily spread, and the spread is seasonal: wide and reliable in winter evenings, wide and negative-skewed on sunny spring weekends when wholesale goes below zero around midday.

Tomorrow’s rates publish around 16:00 for the period from 23:00 to 23:00. That gives you a firm 31-hour horizon at best and a 7-hour horizon at worst, which is why any optimiser you build needs a fallback price model for the tail of its 48-hour window.

A worked day: 11 February 2026

Household: 5.2 kWp south-west array, 9.5 kWh usable battery, 3.68 kW hybrid inverter, region J, annual consumption 4,800 kWh. PV that day, 3.1 kWh. Total load, 14.2 kWh.

time    import_p  load_kWh  pv_kWh  batt_kW  soc_%  grid_kWh  cost_£
01:30      9.1      0.21     0.00    +3.00     12     1.71     0.156
02:00      7.9      0.19     0.00    +3.00     28     1.69     0.134
02:30      8.4      0.18     0.00    +3.00     44     1.68     0.141
03:00      9.6      0.18     0.00    +3.00     60     1.68     0.161
03:30     10.8      0.20     0.00    +3.00     76     1.70     0.184
04:00     11.2      0.22     0.00    +2.30     88     1.37     0.153
...
17:00     38.6      1.45     0.00    -2.90     54     0.00     0.000
17:30     41.2      1.62     0.00    -3.24     37     0.00     0.000
18:00     36.4      1.38     0.00    -2.76     22     0.00     0.000

Charging 9.5 kWh across 01:30 to 04:30 at an average 9.4p costs 89p. At 88% round trip that delivers 8.36 kWh. Of it, 6.8 kWh displaces load between 16:00 and 21:00 at an average 33.1p (£2.25 avoided), and 1.56 kWh covers the 07:00 to 09:00 shoulder at 26p (41p avoided). Gross benefit £2.66, minus 89p of charging, minus 67p of wear at 8p per delivered kWh. Net £1.10.

Now price the same day on Octopus Go: 9.5 kWh in the 00:30 to 05:30 window at 8.5p is 81p, displacing an average day rate of about 30p across the same 8.36 kWh, so £2.51 avoided, £1.03 net after wear. Seven pence apart.

That result is the honest one and it’s worth internalising: on a flat, grey winter day a fixed cheap-rate tariff matches Agile almost exactly. Agile earns its keep on volatile days, on shoulder-season afternoons with sub-zero wholesale, and when paired with Outgoing Agile export. Across a year, a battery-only household typically sees £40 to £90 in Agile’s favour, plus the attention cost of running it. If you also charge an EV, Intelligent Octopus Go at roughly 7p for six hours usually beats Agile outright for that load, and the interesting question becomes whether to split your house and car across tariffs at all.

The overnight charge target is a forecasting problem

Charging to 100% every night is wrong from roughly mid-February to late October. Charge too full and you have no headroom for the roof, so PV that should have been stored gets exported instead. Charge too little and you buy the evening at peak rates.

Price both errors per kWh, assuming a 33p peak, 9p overnight, 88% round trip, 8p wear:

Undercharging costs 33 − (9 ÷ 0.88) − 8 = 14.8p per kWh short. You pay peak instead of off-peak, but you do avoid the wear.

Overcharging depends entirely on your export rate. On Outgoing Octopus at 15p, you spent 10.2p to obtain a kWh you’d have got free from the roof, and you earn 15p exporting the displaced PV, so the net is a 3.2p loss per kWh over. On a 4.1p SEG rate, the same error costs 10.2 + 8 − 4.1 = 14.1p.

This is a newsvendor problem, so the optimal charge target is the critical fractile of your forecast distribution of net evening requirement:

Outgoing at 15p :  14.8 / (14.8 + 3.2)  = 0.82  -> charge to the p82 of net need
SEG at 4.1p     :  14.8 / (14.8 + 14.1) = 0.51  -> charge to the median

Same house, same battery, same weather, and a good export rate moves your target up by roughly a third of a pack. Caveat worth holding: the newsvendor model treats stored energy as perishable, and it isn’t, since surplus charge carries into tomorrow. That biases the true optimum slightly above these fractiles, more so in winter when consecutive dull days are likely.

Forecasting PV well enough to act on

Solcast’s hobbyist tier gives you two rooftop sites and 10 API calls a day at 30-minute resolution, returning pv_estimate10, pv_estimate and pv_estimate90. Those percentile bands are exactly the input the fractile calculation above needs, and they’re the reason Solcast is worth the setup over Forecast.Solar for this specific job.

Tune your site parameters against reality rather than the installer’s paperwork. Run three months of Solcast p50 against your inverter’s actual generation, then adjust declination, azimuth and loss factor until the bias is under 5%. Shading is where most people’s forecasts break: a chimney that clips two strings between 15:00 and 16:00 in October is invisible to Solcast and will show up as a consistent late-afternoon overestimate.

For years you don’t have inverter data for, PVGIS seriescalc gives free hourly irradiance from the SARAH3 database back to 2005, which is enough to synthesise a defensible PV series for backtesting.

Export changes the plan more than import does

Four options behave differently enough that they’re genuinely separate strategies.

Outgoing Octopus at a flat rate is simple and makes overcharging cheap, which is why it pairs well with aggressive overnight targets. Outgoing Agile tracks day-ahead and produces evening peaks that can exceed 20p, which turns your battery into a two-sided instrument: buy at 02:00, sell at 17:30, never touch your own load. Flux bundles import and export into three fixed periods with a high export rate from 16:00 to 19:00, trading upside for predictability. A basic SEG rate at 4p to 6p makes exports nearly worthless and pushes every decision toward self-consumption.

Run the discharge-to-grid trade explicitly. On a day where Outgoing Agile hits 24p at 17:30 and you charged at 9p, the margin is 24 − 10.2 − 8 = 5.8p per kWh. Positive, but 8.36 kWh of it is 48p, and you’ve spent a full cycle. The same cycle spent displacing a 38p import earns £1.65. Export arbitrage only wins when your own load is small or the export spike is genuinely large.

Negative import prices are the exception where everything aligns. On a sunny Sunday with wholesale below zero, you can be paid to charge and then paid again to export in the evening. The conflict to watch is that your roof wants to fill the same pack at the same time, so grid-charge only the headroom your PV forecast says you won’t fill yourself.

Building the simulator without the classic bugs

A 30-minute state machine is enough. Order of operations inside each interval: PV serves load first, surplus charges the battery up to min(charge_power_kW × 0.5, headroom ÷ charge_efficiency), the remainder exports. Any load deficit is served by the battery up to the discharge power limit and the SOC floor, and whatever’s left is imported.

Four bugs account for most wrong answers. Applying round-trip efficiency twice, once on charge and once on discharge, when the 88% figure from the datasheet is already round trip; if you want to split it, use sqrt(0.88) = 0.938 each way. Ignoring the inverter AC limit, which on a 3.68 kW unit caps you at 1.84 kWh per half hour, so a “full charge in one hour” plan is fiction. Forgetting inverter standby draw, which at 45 W is 394 kWh a year and shows up as a stubborn baseline import your model never predicts. And assuming simultaneous grid-charge and PV-export, which many hybrid inverters simply will not do.

Validate against a hand-built spreadsheet. Pick six consecutive intervals spanning a charge-to-discharge transition, compute them manually, and assert your code matches to the nearest 0.01 kWh. That single test catches more than any amount of reading the code.

Backtesting without fooling yourself

Twelve months minimum, because a battery’s value is concentrated in about 120 winter evenings and a summer-heavy sample will mislead you badly.

The bias that ruins most backtests is behavioural. Your consumption history was recorded under your current tariff. Move to Agile and you will shift the dishwasher, run the tumble dryer at 02:00, and stop boiling the kettle at 17:30. Simulating your old load curve on a new tariff overstates the saving, because the real saving includes changes your history doesn’t contain. The fix is to model the shiftable loads separately: identify them (a 1.1 kWh dishwasher cycle, a 3.2 kWh dryer cycle), remove them from the fixed load, and let the optimiser place them.

Test the plan against forecasts, not hindsight. EMHASS makes this distinction explicit with perfect-optim versus naive-mpc-optim, and the gap between them is your forecast penalty. If perfect foresight saves £480 a year and rolling MPC with real Solcast forecasts saves £390, then £90 is the prize for better forecasting and no amount of solver tuning will recover it.

Letting software drive

Predbat, running as an AppDaemon app under Home Assistant, is the most complete option for UK Agile users. It builds a 48-hour plan from Solcast forecasts, your load history and published rates, then writes charge windows and target SOC straight to the inverter. The settings that matter most are metric_battery_cycle (your 8p), best_soc_keep (a reserve for outages), battery_loss and inverter_loss (set from measured data, not the datasheet), and rate_low_threshold.

EMHASS takes a different route: a mixed-integer linear program solved with CBC, which handles deferrable loads and thermal constraints more cleanly than a heuristic planner. It costs more setup and rewards households with a heat pump or an EV.

BottlecapDave’s Octopus Energy integration is the data layer under both. Its target-rate binary sensors (“cheapest continuous 3 hours before 06:00”) are enough to run a decent manual strategy on their own, and its cost-tracker and tariff-comparison sensors give you the counterfactual you need for monitoring.

Using an LLM on this without getting burned

Never ask a model for current tariff rates or product codes. It will produce something plausible, with the right number of decimal places, and it will be wrong. Fetch prices from the API every time.

Where a model earns its place is code. Hand it the JSON schema from the rates endpoint, your battery spec, and your inverter’s power limits, then ask for a vectorised pandas simulator with every assumption declared in a block at the top of the file. Ask for the reconciliation report in the same pass: simulated import kWh against metered import kWh for the same period. Then check the six hand-computed intervals.

Second-model review works well on state machines specifically, because the failure modes are logical rather than syntactic. Give the reviewer your worked example and the code, and ask it to find the interval where they disagree.

Three things worth monitoring forever

Cumulative actual cost against a flat-tariff counterfactual, in pounds, plotted daily. Without it you will never know whether Agile is winning, and the answer changes with the seasons.

Cumulative battery throughput against your warranty allowance. If your warranty implies 2,900 kWh a year and aggressive arbitrage is putting 3,600 kWh through the pack, you are trading warranty cover for pennies per cycle and should raise metric_battery_cycle until the optimiser backs off.

Planned end-of-peak SOC against actual, every day. A scatter of these two, with a 45-degree line, exposes systematic bias in minutes. Points consistently below the line mean your load forecast is too low or your discharge limit is binding; points above mean you’re leaving capacity unused and your charge targets are too conservative.

When your monthly simulated import and your metered import disagree by more than 3%, stop optimising and find the discrepancy. It is almost always one of four things: standby draw you haven’t modelled, a DoD floor higher than you think, an inverter power limit clipping your discharge during the 17:00 to 18:00 hour, or a clock-change day your resampler mangled. Diff the two series half-hour by half-hour, sort by absolute error, and look at the top twenty rows.

In this section

The supporting pages under this subject.