Skip to content

Octopus API: a just-finished half hour of a running Intelligent dispatch is re-priced at day rate until completedDispatches lists it #5520

Closed

Description

@springfall2008

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 in planned_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's completedDispatches feed 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_cost multiplies 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 completedDispatches covers 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

Activity

  1. springfall2008 commented on Oct 12, 2026

    @springfall2008
    OwnerAuthor

    🤖 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 completedDispatches feed 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, and today_cost() re-prices them from the live rate table — so cost_today_import steps 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:

    1. Planned dispatches are deliberately not promoted into 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 through trim_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).
    2. fetch.py:1859/1863 merges completed (supplier feed) + planned into octopus_slots, and rate_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 at octopus.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.
    3. today_cost() (apps/predbat/output.py:2127-2156) prices every elapsed minute of the day from the live self.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.
    4. The recovery path also matches: once completedDispatches lists a half hour it re-enters via fetch.py:1859, where an ended dispatch is off-peak-eligible regardless of location (dispatch_billed_off_peak(), octopus.py:3445-3446).
    5. The post-midnight consequence holds too: calculate_yesterday() rebuilds the previous day from the recorded predbat.cost_today sensor history (output.py:3204 and 3236-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 (via tools/triage_test.sh) — all pass (0.02s). The module covers the slot cap, off-peak eligibility and io_adjusted flagging, but nothing simulates a trimmed or dropped in-progress dispatch, so this gap is untested on main.

    Related, none a duplicate:

    The suggested fix direction — a short-lived, pricing-only record of the elapsed span, dropped when completedDispatches covers 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions