Skip to content

feat(octopus): octopus_intelligent_dynamic - confirm Intelligent slots only well inside a dispatch - #5248

Merged
springfall2008 merged 7 commits into
mainfrom
fix/octopus-intelligent-dynamic
Sep 26, 2026
Merged

springfall2008 merged 7 commits into
mainfrom
fix/octopus-intelligent-dynamic

Conversation

@springfall2008

@springfall2008 springfall2008 commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Follow-up to #5229 and #5245.

Problem

#5229 checks Octopus Intelligent slots against whether the car is actually charging, and cancels them if it isn't. Two things stopped it working in practice.

  1. It never ran on real installs. It was gated on switch.predbat_metric_dynamic_load_adjust, which defaults Off. predbat_config.json saves every setting, defaults included, so almost every install has false stored, and changing the default would only reach new installs. A live test on a Wallbox showed exactly this: the car was in a dispatch at 0 W, and not one car ... decision line was ever logged.
  2. It judged dispatches on a 5-minute grid. Octopus schedules them to the minute. The dispatch in that test was two entries, 15:55–16:00 and 16:00:00–16:01:22, about 6.5 minutes in all. "In a slot" was checked against minutes_now, rounded down to 5 minutes. So a dispatch ending at 12:37 still looked in progress until 12:40, and the car stopping because it ended counted as "not charging". There was also no allowance for the car and charger waking up at the start.

Change

  • New switch.predbat_octopus_intelligent_dynamic (expert mode, default On) gates slot confirmation. It still only runs for an Intelligent car with evidence: a real car_charging_now entity, or the house load with the car inside the CT clamp. So existing installs with a sensor get it without changing a setting.
    • metric_dynamic_load_adjust is now only the load estimate.
    • The trust switch's Off mode needs the new switch. With it Off, a warning says so.
  • "In a slot" uses the exact clock (now_utc_real plus skew), in the check and the 15-second poll alike, so the two still agree. Zeroing slots and building rates keep minutes_now.
  • Back-to-back slots count as one dispatch, so a long dispatch split into half hours has no start band at each join.
  • Start band for the sensor: a "not charging" reading counts from 3 minutes into the dispatch, then the 2-minute grace applies, so the earliest cancel is start + 5.
    • An in-progress dispatch's start can be advanced to now each cycle, so the earliest start seen is kept, keyed on the dispatch's end.
    • "Charging" still counts anywhere in the dispatch.
  • Load window: a load reading is the 5-minute average up to minutes_now, and only counts when that whole window lies inside the dispatch. The grace is one more window, timed on the 5-minute grid rather than the exact clock, because load only arrives on cycles. So two low windows in a row cancel: start + 10 for a dispatch starting on the grid.
  • No end band: a dispatch is simply over at its real end.
  • Docs: a new "Checking Intelligent dispatches against the car" section under Octopus-led charging covers why the check exists, setup (including a charging-power sensor), how it decides (a timing table), what happens when a slot is cancelled, a worked example, and troubleshooting. The switch bullets link to it.

Live regression

A second live test on main showed a cancellation being dropped although car_charging_now never changed:

  • 16:42: the car was cancelled against the planned 16:30–17:00 entry.
  • 16:43: Octopus replaced that entry with a started one, 16:40:39–17:00.
  • Main judged this on minutes_now, rounded down to 16:40:00, which is before 16:40:39. So the car appeared to have left the slot, and it resumed.

Test 33b reproduces this: it passes here, and fails with the rounded clock. Here it's fixed twice over: by the exact clock, and by the dispatch start remembered against the unchanged 17:00 end.

Behaviour changes

  • On by default: Intelligent users with a real car_charging_now sensor now get slot confirmation without changing a setting.
  • Load-based confirmation is on by default too: Intelligent users whose car is inside the CT clamp and who have no sensor now get it, where before they needed dynamic load.
  • Dynamic load no longer affects car slots: users who had it on only for the load estimate see no change there.
  • The load method is quicker: start + 10 on the grid, down from a flat 10 minutes of low load, and it now ignores load from before the dispatch started.

Tests

test_dynamic_load_car_not_charging (51 scenarios). _reset() now leaves dynamic load off, so every scenario shows the feature working without it. New cases:

  • the start band: no cancel before start + 3, a cancel at start + 5, and "charging" counting inside the band;
  • back-to-back slots;
  • a start that drifts forward;
  • the 6.5-minute dispatch from the live log, cancelled at 16:00:15 and over at 16:01:22;
  • a 12:23–12:37 dispatch that's over at 12:37:10, in both the poll and the check;
  • a load window straddling an off-grid start, and grace timed on the grid;
  • dynamic load off still working, and the new switch off (with the trust switch Off) doing nothing;
  • the switch-off warning.

Deliberately broken versions of the start band, the merging, the remembered start, the exact clock, the load window, the load timing and the switch gate each make it fail. ./run_all --quick: all passed. pre-commit: clean.

🤖 Generated with Claude Code

…s only well inside a dispatch

Checking Octopus Intelligent slots against whether the car is charging (from
#5229) was gated on metric_dynamic_load_adjust, which defaults Off, and
predbat_config.json persists every setting, so almost no install had it on.
A live test on a Wallbox found nothing was ever cancelled for exactly that
reason.

- New switch.predbat_octopus_intelligent_dynamic (expert mode, default On) gates
  it instead. It still only runs for an Intelligent car with evidence: a real
  car_charging_now entity, or the house load with the car inside the CT clamp.
  So existing installs with a sensor get it without changing a setting.
  metric_dynamic_load_adjust is now only the load estimate. The trust switch's
  Off mode needs the new switch, and warns when it is Off.

Octopus schedules dispatches to the minute (the live one was 15:55-16:01:22, two
entries), so the check now judges them that way:

- "In a slot" uses the exact clock (now_utc_real plus skew), not minutes_now
  floored to 5 minutes, in the check and the poll alike. A dispatch ending at
  12:37 is over at 12:37, not 12:40, and the car stopping because it ended is
  no longer read as "not charging".
- Back-to-back slots are one dispatch, so a long dispatch split into half hours
  has no start band at each join.
- A sensor's "not charging" only counts from 3 minutes into the dispatch, while
  the car and charger wake up (earliest cancel start + 5). An in-progress
  dispatch's start can be advanced to now each cycle, so the earliest start seen
  is kept, keyed on the dispatch's end.
- A load reading is a 5 minute average and only counts when that whole window
  lies inside the dispatch. The grace is one more window on the 5 minute grid,
  so two low windows in a row cancel: start + 10 on the grid.
- "Charging" from the sensor still counts anywhere in the dispatch. There is no
  band at the end.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@springfall2008 springfall2008 added the BOT_REVIEW Trigger an autotriage label Sep 26, 2026
CI and others added 4 commits September 26, 2026 16:43
A new "Checking Intelligent dispatches against the car" section under
Octopus-led charging covers:

- why the check exists (a dispatch the car does not use may be billed at the
  full rate);
- how to set it up, with a charging-power sensor example;
- how it decides (the sensor and load timings as a table);
- what happens when the car is not charging, and what the plan and log show;
- a worked example of a 15:55-16:01 dispatch;
- troubleshooting for when nothing is ever cancelled, including that the
  "Dynamic load last period" line does not show the check is running.

The switch bullets now point to that section instead of repeating it. The
rate-strip log line now shares the "Octopus Intelligent:" prefix with the
others.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ncellation

Reproduces a live sequence on main. The car was cancelled at 16:42 against the
planned 16:30-17:00 entry, then at 16:43 Octopus replaced it with a started
entry 16:40:39-17:00. Judged on minutes_now floored to 16:40, that looked like
leaving the slot, and the cancellation was dropped although car_charging_now
never changed. The exact clock and the remembered dispatch start keep it; the
test fails with the floored minute.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test_snapshot_time_formatting searched the whole snapshot for "1590"/"1860",
the charge window's raw minute counts. The snapshot also carries the Predbat
version with its git hash, so the test failed whenever CI's merge commit hash
contained either string: PR #5248's run hashed to 3315908. It now checks only
the planned-window lines, and pins that version string so the case is covered
for good. Reverting format_window to raw minutes still fails it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread apps/predbat/plan.py
Comment thread apps/predbat/plan.py Outdated
changed = True
if cancelled and self.dynamic_load_car_in_slot(car_n, self.minutes_now):
reason = "is in a charging slot but not charging, cancelling its slots"
if cancelled and self.dynamic_load_car_in_slot(car_n, minute):

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: for a cancelled car the merged dispatch is computed and the slot list sorted twice in this cycle - once inside dynamic_load_car_target (line 510) and again here via dynamic_load_car_in_slot - and dynamic_load_car_dispatch re-sorts the slots per car every 15 seconds in the poll. dynamic_load_car_in_slot (line 459) now has exactly one caller, this line. Reusing the dispatch tuple the target call already produced (or dropping the wrapper) removes the duplicate sort/merge.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reply from Claude Code, working with @springfall2008.

Fixed in 2989f8e. dynamic_load_car_in_slot() is removed, and its one caller uses dynamic_load_car_dispatch() directly. That call only runs when a car's state changes, to choose the log wording, so the extra merge doesn't happen on ordinary cycles. I left the poll's per-car sort as it is: it sorts a handful of one car's slots every 15 seconds, which isn't worth caching state for.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reply from Claude Code, working with @springfall2008.

Fixed in 2989f8e: dynamic_load_car_in_slot() is gone, and its one caller now calls dynamic_load_car_dispatch(car_n, minute) is not None directly. That call only runs when a car's cancellation state changes, to choose the log wording, so the extra sort happens rarely. The poll's per-car sort is over a car's own few slots every 15 seconds, so I've left it as is.

Comment thread apps/predbat/plan.py
@springfall2008 springfall2008 removed the BOT_REVIEW Trigger an autotriage label Sep 26, 2026
CI and others added 2 commits September 26, 2026 20:27
…onfig warnings

- dynamic_load_car_in_slot() had a single caller, the log wording on a state
  change, so it now calls dynamic_load_car_dispatch() directly.
- dynamic_load_car_check_config()'s docstring described only the cars list; it
  now covers all three warnings it logs and what it returns.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant