fix(octopus): stop IOG dispatch slots becoming a permanent off-peak window - #4854
Conversation
…indow 01e3982 started deriving a meter's off-peak windows from the measurements API's TOU bucket labels, taking whichever label was most common for each half-hour across the last 7 days. That premise holds for Economy 7, where the labels really do describe a fixed schedule, but not for Intelligent Octopus Go. On IOG the off-peak buckets cover the guaranteed overnight window plus whatever ad-hoc dispatch slots Octopus granted that night, and dispatches are decided night by night. A bonus slot that happened to land on a plurality of the sampled nights was therefore promoted to a permanent recurring window applied to every future day. On one user's account (IOG-SMB-VAR) that invented a nightly 22:00-22:30 off-peak period alongside the genuine 23:30-05:30 one, and Predbat planned a battery charge into it believing the rate was 6.9p while the meter billed the day rate of 30.3p. The plurality vote has no consistency threshold, so four nights out of seven was enough. Skip the inference for Intelligent GO tariffs and use the guaranteed window, which is the only fixed part of an IOG day. octopus_night_times still overrides everything for a meter whose real bands the API does not report. Also anchor TOU-derived windows to whichever clock the tariff's off-peak period is actually fixed to, rather than assuming UTC. The labels are UTC instants, so a local wall-clock tariff sampled during BST reports its window shifted by an hour; holding that offset fixed ran it an hour early once the clocks went back - the same class of bug 01e3982 set out to fix, reintroduced for local tariffs. Verified against a reporting user's live account: 6.9p now applies only across 23:30-05:30 local and 22:00-22:30 is back to the 30.3p day rate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The distinguishing symptom is a cheap slot Octopus has never heard of while the dispatch feeds are empty, which points at the day/night rate path rather than phantom dispatches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The local-anchor conversion uses the current UTC offset, which can mis-convert TOU-derived windows when the 7-day sample straddles a DST transition, and the new log line is misleading about which window values are actually applied.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes a cost-risk bug in the Octopus integration where Intelligent Octopus Go (IOG) ad-hoc dispatch TOU labels could be inferred as permanent recurring off-peak windows, and improves how TOU-derived windows are anchored (UTC vs local wall-clock) to avoid DST-related drift.
Changes:
- Skip TOU-label schedule inference for Intelligent GO tariffs and fall back to the guaranteed hard-wired IOG window.
- Anchor TOU-derived windows to the tariff’s clock basis (UTC-fixed vs local-fixed) and add a helper to convert UTC-minute windows to local minutes.
- Add regression tests covering the IOG phantom-slot case and a local-anchored TOU window spanning a DST change.
File summaries
| File | Description |
|---|---|
| apps/predbat/octopus.py | Skips TOU inference for IOG; anchors TOU-derived windows to UTC vs local and converts local-anchored windows. |
| apps/predbat/tests/test_octopus_tou_windows.py | Adds two new regression tests for IOG phantom off-peak slot and DST anchoring behavior. |
| .claude/skills/issue-triage/references/debug-journal.md | Documents the observed IOG TOU-label inference failure mode and DST anchoring nuance for future triage. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…bels For a local-anchored tariff, the log printed tou_windows (raw UTC minutes) while windows (converted to local wall-clock minutes) was what actually got applied, making the log line misleading during debugging.
Problem
01e39825began deriving a meter's off-peak windows from the measurements API'sTOU_BUCKET_COSTlabels, taking whichever label was most common for each half-hour across the last 7 days. That premise is sound for Economy 7 — the labels genuinely describe a fixed schedule — but it was applied to every day/night tariff, including Intelligent Octopus Go.On IOG the off-peak buckets cover the guaranteed overnight window plus whatever ad-hoc dispatch slots Octopus granted that night. Dispatches are decided night by night, so a bonus slot that happened to land on a plurality of the sampled nights was promoted to a permanent recurring window applied to all future days. The vote is a plain plurality (
octopus.py_tou_windows_from_measurements) with no consistency threshold and no minimum sample count, so four nights out of seven was enough.On one user's account (
IOG-SMB-VAR) this invented a nightly 22:00–22:30 off-peak period alongside the genuine 23:30–05:30 one:(1260, 1290)is 22:00–22:30 BST and is not part of any guaranteed IOG schedule. Predbat did not merely display it — it planned a battery charge into that window believing the rate was 6.9p while the meter billed the day rate of 30.3p. This is a live cost-risk bug, not a cosmetic one.Note this is not the phantom-dispatch mechanism: it needs no dispatch data at all. The account in question reported empty
flexPlannedDispatches/completedDispatcheson every poll.Fix
octopus_night_timesstill overrides everything, for a meter whose real bands the API does not report.utc=True. The labels are UTC instants, so a local wall-clock tariff sampled during BST reports its window shifted by an hour; holding that offset fixed ran it an hour early once the clocks went back — the same class of bug01e39825set out to fix, reintroduced for local tariffs.GO/E7/E10 meters keep reading their real windows from the meter, which is what
01e39825was for.Tests
Two new cases in
test_octopus_tou_windows.py, both watched failing first:./run_all --quick: 298 passed, 0 failed.run_pre_commitclean.Verified against a live account
6.9p now applies only across the 12 half-hours of 23:30–05:30 local; 22:00–22:30 is back to 30.27p.
Known limitation, not addressed here
A local-anchored window that spans the 02:00 DST changeover comes out an hour short on that one day, because the schedule builder adds a
timedeltato an already-localised midnight and keeps that day's opening UTC offset. This is pre-existing and unrelated to this change — the hard-wired GO window produces the identical two entries on that date with no TOU labels involved. Test 13 excludes the changeover day for that reason. Worth a follow-up.🤖 Generated with Claude Code