Repository navigation
Don't trust Intelligent slots the car won't need (consider_full) - #5403
Conversation
With octopus_intelligent_consider_full on, load_octopus_slots() already ends each car's slots where it reaches its limit, so the car is modelled not to draw in the rest - but every dispatch minute still kept its cheap rate, and the house battery planned around slots Octopus only bills cheap when the car charges in them. octopus_surplus_minutes() works out the future dispatch minutes no car still needs (rounded out to whole half hours, shared across cars, outside the fixed 23:30-05:30 window), and both rate paths now withhold them the way they already do for a cancelled car: rate_add_io_slots() doesn't stamp them, and dynamic_load_car_strip_feed_rates() removes a discount the rate feed already delivered. Elapsed minutes keep their rate so today's cost stays true. Behaviour is unchanged with consider_full off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r-need gate dynamic_load_car_check() cancels a car by zeroing its future slots and keeping the kWh in kwh_cancelled. The car-need gate read that zero as "the car won't need it", so with octopus_intelligent_consider_full on it stripped the cheap rate from the rest of the half hour #5229 deliberately keeps for a car seen charging in it. Cancellation decides a cancelled car's rate itself; the gate now only answers whether a slot is beyond the car's need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The surplus started at minutes_now, which usually falls mid-period, so the rest of the current half hour lost its low rate once the car was full - even when the car charged earlier in it, which makes Octopus bill the whole period cheap. Surplus now starts at the next half-hour boundary. Clamping back to the start of the current period instead would have marked elapsed minutes as surplus too, and rate_add_io_slots() applies the skip to past minutes, misreporting today's cost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Boundary handling, active-dispatch capping, and cross-car eligibility can produce incorrect electricity prices.
Review effort: Balanced
Findings: 4
Open (4)
What changed in this PR
Links Intelligent Octopus rate planning to the car’s predicted charging need when consider_full is enabled, addressing #4482.
Changes:
- Withholds surplus dispatch discounts from both rate paths.
- Shares fixed-window handling and initializes surplus tracking.
- Adds regression tests and updates charging documentation.
| File | Description |
|---|---|
| docs/car-charging.md | Documents need-based discount handling. |
| apps/predbat/unit_test.py | Registers the new tests. |
| apps/predbat/tests/test_iog_car_need_gate.py | Tests surplus detection and rate handling. |
| apps/predbat/predbat.py | Initializes surplus-minute state. |
| apps/predbat/octopus.py | Calculates surplus minutes and adjusts discounts. |
| apps/predbat/fetch.py | Calculates surplus before processing rates. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…review) Copilot's review of the gate found four ways it could misprice a dispatch: - The surplus started at the next half-hour boundary computed as a ceiling, which is the current boundary when the floored clock sits on one (a fetch at 14:02 reads 14:00), so the current half hour could still lose its rate. It now starts at the strictly next boundary. - A cancelled car's kwh_cancelled counted as needed for the whole slot, so it kept a full car's surplus minutes cheap though neither car would charge then. A car's need now counts only before its own cheap rate ends (dynamic_load_car_strip_from()), which still keeps the half hour GH#5316 keeps for a car seen charging in it. - A car not modelled from its Octopus slots was simply skipped, so a full car's overlapping surplus stripped its discount too. Its dispatches are now kept, up to its own strip_from. - An untrimmed dispatch in progress (octopus_intelligent_slot isn't trimmed to now, unlike the Octopus API component's) is capped from its original start, so the capped slot can end before the car finishes. The running dispatch is now left alone; dynamic load cancels it in real time once the car stops. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
springfall2008
left a comment
There was a problem hiding this comment.
Two things from reviewing this at 380117c. Neither affects anyone with octopus_intelligent_consider_full Off, so they don't block on safety, but the first limits how much of #4482 this closes.
1. The gate switches off when a single long dispatch starts
A dispatch in progress is exempt from the gate for its whole length (in_progress in octopus_surplus_minutes()). So the surplus is withheld right up until the dispatch begins, and comes back the moment it does.
Reproduced against this branch with the real load_octopus_slots(), octopus_surplus_minutes() and rate_add_io_slots(). Car at 55 of 60 kWh (needs 5 kWh), consider_full On, no cancellation:
| Dispatches from Octopus | Clock | Cheap | Surplus (withheld) |
|---|---|---|---|
| One, 13:00-17:00, 28 kWh | 12:55 | 13:00-14:00 | 14:00-17:00 |
| One, 13:00-17:00, 28 kWh | 13:05 | 13:05-17:00 | none |
| Eight half hours, 13:00-17:00, 3.5 kWh each | 12:55 | 13:00-14:00 | 14:00-17:00 |
| Eight half hours, 13:00-17:00, 3.5 kWh each | 13:05 | 13:05-14:00 | 14:00-17:00 |
So the result depends on how Octopus happens to chunk the charge. With one long dispatch, at 13:05 the plan sees three more cheap hours that the car won't charge in, which is the sequence #4482 describes (hold the battery now, plan to charge it later in the dispatch, the slots then go). After that point only octopus_intelligent_dynamic can remove the rate, and only once the car has stopped and there is a car_charging_now sensor or CT reading to see it.
The reply on Copilot's thread says the exemption was chosen to avoid changing load_octopus_slots(). I think the suggestion there is the right fix: work out a running dispatch's need from its remaining span and energy, so the capped slots are reliable mid-dispatch and the exemption isn't needed. The Octopus API component already trims a running dispatch to now (trim_started_dispatch()), so on that path the capped slots are already right and the exemption is only costing accuracy. If you'd rather keep it out of this PR, a narrower step would be to exempt only what the gate can't trust: an untrimmed dispatch from octopus_intelligent_slot. Either way the docs should say that a dispatch already running keeps its low rate for its full length.
2. A change in the surplus doesn't force a replan
fetch_sensor_data() forces a replan when the dispatch signature changes or when dynamic_load_car_check() reports a change, but not when octopus_surplus changes. The rates move without the slots moving in two cases here: the car's SoC changing how many half hours it needs, and the flip in point 1 when a dispatch starts. The plan then stays as it was until the normal calculate_plan_every timer, which can be set as high as 60 minutes.
Suggest comparing the new surplus with the previous cycle's where it is assigned in fetch_sensor_data() and setting force_replan when it differs, as is done for dynamic_load_car_changed. It needs the save guard the other checks have, so a compare.py run doesn't trigger it.
…surplus changes (#5403 review) Trefor's review of #5403: 1. A dispatch in progress was exempt from the gate for its whole length, so one long dispatch kept all its surplus cheap from the moment it started, while the same charge chunked into half-hour dispatches didn't. With consider_full on, load_octopus_slots() now caps a running dispatch's need from its remaining span and energy (as trim_started_dispatch() already does for the Octopus API component, but octopus_intelligent_slot doesn't), after the slot-cap chunking, which still counts it from its real start. The exemption is gone. 2. A change in the surplus didn't force a replan - the rates can move without the slots moving, as the car's SoC changes how many half hours it needs. fetch_sensor_data() now replans when it changes, guarded by save like dynamic_load_car_check(). Only the part of the previous surplus still ahead is compared, so time passing alone isn't a change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Posted by Claude on behalf of @chalfontchubby. Thanks. Both are fixed in 22c1a14. 1. The gate switching off when a long dispatch starts. I've taken the fix you preferred and removed the exemption. With Your table is now a regression test, using the real 2. Replan on a surplus change. |
|
Thankyou ❤️ |

Posted by Claude on behalf of @chalfontchubby.
Closes #4482.
Summary
With
octopus_intelligent_consider_fullOn, Predbat already models the car stopping once it's full:load_octopus_slots()ends the car's slots there, so the car load and "Hold for car" stop. But every dispatch minute kept its low rate, so the house battery was still planned around slots the car won't charge in. Octopus only bills a dispatch slot cheap when the car actually charges in it.This makes the rate agree with what
consider_fullalready decided. Future dispatch minutes that no car still needs are planned at the normal rate, the same way a cancelled car's are (#5229).It mostly matters for charger-led Intelligent setups (Zappi, Ohme and similar), where Octopus doesn't know the car's state of charge and allocates more slots than the car needs, which is the case
consider_fullexists for. Car-led integrations size the dispatch from the car's own SoC, so there's little surplus.Behaviour is unchanged with
consider_fullOff (the default).Details
octopus_surplus_minutes()collects the future dispatch minutes that no car can use. It decides per car, then shares the result, because rates are shared between cars. A car can use:kwh_cancelledfor a cancelled car;Each car counts only up to its own
dynamic_load_car_strip_from(), so cancellation keeps its say. That includes the half hour GH#5316 keeps cheap for a car seen charging in it. Minutes are rounded out to whole half hours, the fixed 23:30-05:30 window is excluded, and the surplus starts from the half hour after the current one, so the current half hour and elapsed minutes always keep their rate.A dispatch in progress is judged from now. With
consider_fullOn,load_octopus_slots()caps a running dispatch's need from its remaining span and energy, the waytrim_started_dispatch()already does for the Octopus API component (octopus_intelligent_slotdispatches aren't trimmed). This happens after the daily slot-cap chunking, which still counts the dispatch from its real start. So one long dispatch and the same charge split into half hours get the same answer.A change in the surplus forces a replan, because the rates can move without the slots moving (the car's SoC changes how many half hours it needs). Only the part of the previous surplus still ahead is compared, so time passing alone doesn't trigger it.
Both rate paths withhold the surplus:
rate_add_io_slots()doesn't stamp it;dynamic_load_car_strip_feed_rates()removes a discount the rate feed already delivered.in_iog_fixed_window()is factored out ofdynamic_load_car_strip_feed_rates(), which now shares it.Effect of the two settings together (with
octopus_intelligent_dynamicOn)Possible follow-up
consider_fullonly affects Intelligent slots, so together withoctopus_intelligent_trust_slotsit's really one setting: which Intelligent slots to trust. They could be merged into one select (All / Needed / Confirmed). That needs a migration from the two switches, so I've left it out here. Happy to do it if you'd like it.Background
#4885 proposed a separate
limit_future_slotsswitch for this. This version hangs off the existingconsider_fullswitch instead, because with it On the model already says the car won't charge in those slots.Test plan
tests/test_iog_car_need_gate.py(iog_car_need_gate):strip_from, and a car that isn't modelled (alone, overlapping a full car, and cancelled);load_octopus_slots()capping, for both Predbat's own overlay and a feed-delivered discount, including an untrimmed dispatch already in progress and a full car sharing a dispatch with a cancelled car;consider_fullOff unchanged../run_all --quickgreen../run_pre_commitgreen on every commit.🤖 Generated with Claude Code