Repository navigation
Octopus API: a just-finished half hour of a running Intelligent dispatch is re-priced at day rate until completedDispatches lists it #5520
Description
Summary
With the built-in Octopus API component (Octopus or another Kraken supplier), a half hour of a running Intelligent dispatch is priced at the off-peak rate while it is live, then loses that rate as soon as it has passed. It only gets it back when the supplier lists the half hour in completedDispatches, which here took 15 to 60 minutes. cost_today is recalculated every cycle from the current rate table, so during a genuine car charge the day's cost jumps up after each half hour and falls back later.
Nothing is wrong with the plan or with what the battery does. The fault is in the displayed cost and the plan history for the hour or so after each half hour of charging.
What was observed
E.ON Next Drive Smart (Kraken), myenergi Zappi under supplier smart charging, v9.3.7. Off-peak 6.57p, day rate 27.7p. The car charged from about 17:30 to 18:30 local time at roughly 7 kW. predbat.rates read 6.57p throughout.
| Time | cost_today_import (p) |
import_energy_h0 (kWh) |
What changed |
|---|---|---|---|
| 17:59 | 374.74 | 55.1 | accruing at 6.57p |
| 18:04 | 482.08 | 56.0 | 17:30–18:00 re-priced at 27.7p (+107p) |
| 18:24 | 505.07 | 59.5 | accruing at 6.57p |
| 18:29 | 601.33 | 60.0 | 18:00–18:30 re-priced (+96p) |
| 18:39 | 609.46 | 60.7 | |
| 18:44 | 414.16 | 61.1 | completed dispatches for 17:30–18:30 arrive (−195p) |
| 18:59 | 456.04 | 61.9 | 18:30–19:00 re-priced (+38p) |
When each half hour first appeared in the log as a completed dispatch (location AT_HOME):
| Half hour | Energy reported | First listed | After the slot ended |
|---|---|---|---|
| 17:00–17:30 | 0.35 kWh | 18:30 | 60 min |
| 17:30–18:00 | 3.8 kWh | 18:45 | 45 min |
| 18:00–18:30 | 2.87 kWh | 18:45 | 15 min |
| 18:30–19:00 | 0.26 kWh | about 19:45 | 45 min |
So the day's import cost read about £2 too high at its worst (609p against 414p once corrected), on a day whose true import cost was under £5.
Meanwhile the planned dispatch in the log moved its start forward on every poll:
Intelligent slot at 10-12 18:00:00-10-13 06:00:00, assumed price 6.57, amount 71.07, kWh location None, source SMART
Intelligent slot at 10-12 18:30:00-10-13 06:00:00, assumed price 6.57, amount 70.45, kWh location None, source SMART
Cause
In apps/predbat/octopus.py on main:
trim_started_dispatch()advances the start of a planned dispatch that is in progress to now, so the elapsed part is no longer inplanned_dispatches. A cached planned dispatch that has ended is dropped altogether.- Since Octopus Intelligent: in-progress flexPlannedDispatches promoted to completed creates phantom cheap slots when EV unplugged #4114 an in-progress planned dispatch is deliberately not promoted into
completed_dispatches, so nothing records the elapsed part until the supplier'scompletedDispatchesfeed lists it. rate_add_io_slots()rounds a slot out to the half hour, so the current half hour stays covered, but every earlier half hour of the same dispatch falls back to the base tariff.today_costmultiplies each elapsed minute's energy by the current rate table, so the re-pricing shows immediately.
The comment in rate_add_io_slots() says elapsed minutes keep their cheap rate because "they record what the tariff charged, and today's cost is built from them". That holds for the half hour in progress only.
A second consequence: a completed dispatch that arrives after midnight is too late for the day it belongs to, because that day's total has already been stored.
Suggested fix
Keep a short-lived record of the elapsed part of a dispatch for pricing only, without bringing back the #4114 problem:
- When
trim_started_dispatch()trims a dispatch, or an ended one is dropped, remember the elapsed span per device as "started, awaiting confirmation". - Price those minutes at the off-peak rate only where the car was seen charging in them (the same signal
dynamic_load_car_check()uses), so a slot in which the car drew nothing is not kept cheap. - Drop the record when
completedDispatchescovers the span, or after a timeout (90 minutes would have covered every case above), so an unconfirmed slot reverts to the base rate as it does now. - Never use the record for future minutes or for the daily slot cap.
#5443 did the equivalent for the Ohme planned-slot sensor ("a slot is covered from when it ends until Octopus lists it"); this is the same gap in the Octopus API component.
Related
- Manual rate overrides are dropped once a slot passes, so cost_today re-prices that slot at the base tariff #5517: manual rate overrides are dropped once a slot passes, and
cost_todayre-prices that slot. Samecost_todaymechanism, different source of the rate. - IOG extra car charging slots still not holding the off-peak rate #5413 / fix(ohme): take completed Intelligent dispatches from Octopus, planned slots from Ohme #5443: the same symptom for Ohme-sourced slots.
- Octopus Intelligent: in-progress flexPlannedDispatches promoted to completed creates phantom cheap slots when EV unplugged #4114: why in-progress planned dispatches are not promoted to completed.
- IOG future planned dispatches treated as cheap home rate before EV charging earns the slot #4516: planned dispatches priced as firm off-peak (the planning side of this).
Activity
springfall2008 commentedon Oct 12, 2026
🤖 Automated first-pass triage — a maintainer will review before any action is taken.
Classification: bug. Predbat prices the already-elapsed half hours of a live Octopus Intelligent dispatch at the day rate until the supplier's
completedDispatchesfeed confirms them:trim_started_dispatch()advances the slot's start so those minutes are in neither the planned list nor the not-yet-arrived completed list, andtoday_cost()re-prices them from the live rate table — socost_today_importsteps up during genuine charging and falls back again 15–60 minutes later. Priority: medium (recurring on every Intelligent charging event for users of the built-in Octopus API component, but self-correcting; affects the displayed cost and plan history, not the plan or what the inverter is asked to do).What I investigated (main at 40b9ce2, i.e. current main just past v9.3.7 — the report is against a current version) — every link of your causal chain checks out in the code:
completed_dispatches(apps/predbat/octopus.py:2693-2701, the Octopus Intelligent: in-progress flexPlannedDispatches promoted to completed creates phantom cheap slots when EV unplugged #4114 decision via PR Octopus: stop flex dispatch promotion creating phantom slots (#4114); redact JWT in logs #4115) and the in-progress one goes throughtrim_started_dispatch()(octopus.py:2763), which advances the start to now — so the elapsed part is covered by neither list. A cached planned dispatch that has fully ended is dropped outright (octopus.py:2739-2740).fetch.py:1859/1863merges completed (supplier feed) + planned intooctopus_slots, andrate_add_io_slots()(octopus.py:3974) rounds out only the trimmed start to its half hour — earlier half hours of the same dispatch are stamped at the base tariff. The comment atoctopus.py:3948-3951("elapsed minutes keep theirs – they record what the tariff charged") holds for the half hour in progress only, exactly as you note.today_cost()(apps/predbat/output.py:2127-2156) prices every elapsed minute of the day from the liveself.rate_import— and because the rate table is rebuilt from the tariff each cycle, nothing retains the elapsed minutes' cheap rate across the gap. The jumps and later fall-backs you tabulated follow from that; same underlying mechanism as Manual rate overrides are dropped once a slot passes, so cost_today re-prices that slot at the base tariff #5517.completedDispatcheslists a half hour it re-enters viafetch.py:1859, where an ended dispatch is off-peak-eligible regardless of location (dispatch_billed_off_peak(),octopus.py:3445-3446).calculate_yesterday()rebuilds the previous day from the recordedpredbat.cost_todaysensor history (output.py:3204and3236-3322), so a completed dispatch confirmed after midnight leaves the finished day's total permanently wrong in history/compare.Test: ran
test_rate_add_io_slots(viatools/triage_test.sh) — all pass (0.02s). The module covers the slot cap, off-peak eligibility andio_adjustedflagging, but nothing simulates a trimmed or dropped in-progress dispatch, so this gap is untested on main.Related, none a duplicate:
today_costre-pricing mechanism, different trigger (manual rate overrides dropped once a slot passes).octopus.py:1556) — so there is no second planned-slot sensor covering it the wayoctopus_intelligent_planned_slotdoes atfetch.py:1842-1852.The suggested fix direction — a short-lived, pricing-only record of the elapsed span, dropped when
completedDispatchescovers it or on timeout, never used for future minutes or the daily slot cap — is consistent with the #4114 constraint and the #5443 precedent. No further information needed from you.fix(octopus): only hold a half hour a planned dispatch is running in …
fix(octopus): keep the dispatch rate on an elapsed half hour until Oc…