fix(octopus): don't rely on future IOG dispatch slots the car no longer needs - #4483
Conversation
…er needs For springfall2008#4482: Octopus grants daytime Intelligent Go dispatch slots based on its own assumption of what the car needs, since it can't see the car's real SoC - only Predbat can, via car_charging_soc/car_charging_limit. load_octopus_slots() already caps car_charging_slots[car_n] at the car's real remaining requirement when octopus_intelligent_consider_full is on, zeroing the kwh of any slot beyond that. But rate_add_io_slots() (which decides which dispatch slots the house battery can rely on as a cheap rate) never looked at that - it still treated every granted slot as low rate regardless of whether the car's own plan says it's still needed. If Predbat had already deferred house battery charging into one of those "surplus" slots, the slot could disappear once Octopus notices the car has stopped drawing, leaving the battery undercharged with no cheap window left to make it up. Adds octopus_intelligent_limit_future_slots (expert mode, default Off, requires octopus_intelligent_consider_full to have any effect - warns at startup otherwise): a *future* out-of-window dispatch slot only counts as low rate for the house battery while it falls within a 30-min block car_charging_slots still lists a positive kwh for. A slot already underway or completed is trusted regardless (only future need is gated), and the fixed 23:30-05:30 window is never affected, since it's guaranteed cheap by the tariff itself rather than the dispatch mechanism. Test coverage: 6 new rate_add_io_slots scenarios (partial-need-vs-full settlement period, car already full, current/completed dispatch exempted, switch off restores old behaviour, fixed window unaffected) plus a fetch_config_options warning test for the consider_full dependency. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Can you review this against: #4237 |
|
Checked against #4237 - no overlap or conflict, they're on different pathways:
No file overlap either - #4237 only touches Functionally they're complementary rather than duplicative: this PR answers "should a future out-of-window slot be relied on for the house battery at all" (hard exclusion, gated on the car's real remaining SoC need), #4237 answers "given the slots that are being relied on, which within a run should be preferred first" (soft risk-decay ranking). A slot this PR excludes ( One thing I can't verify from code alone: whether Octopus's |
|
One possible interaction with #4237/the Octopus rate input path may need testing. When metric_octopus_import is used, fetch_octopus_rates() can receive a dispatch period whose value_inc_vat is already the Intelligent low rate and whose is_intelligent_adjusted flag is true. By the time rate_add_io_slots() runs, rates[minute] may therefore already be 3.99p. In that case, needed=False only prevents another low-rate assignment; it does not restore the already-adjusted rate to the normal out-of-window tariff. The block may consequently remain available to the optimiser. I observed this shape in the live Octopus integration: value_inc_vat: 0.03993 compared with the normal peak rate of 0.293222. Could a test be added where the input rate is already low and io_adjusted is true, asserting that an unneeded future out-of-window block returns to the firm tariff? The implementation may also need to clear or ignore the corresponding io_adjusted marker so #4237 does not continue treating the rejected block as an Intelligent window. The existing tests initialise all rates at 10p and rely on rate_add_io_slots() to reduce accepted blocks to 4p, so they do not currently cover this input path. |
|
Excellent catch, thank you - traced it through and you're right.
Proposed fix: when
Will add a test with rates pre-seeded low (mirroring your example) + |
|
I now have a live example confirming this input path and answering the restoration-target question. For tonight, Octopus currently advertises these dispatches:
The car's actual SoC is 2%. With However, the Octopus rate event already supplies that surplus block as: start: 2026-08-13T05:30:00+01:00 |
…ing a new one Review follow-up on this PR (springfall2008#4483): for a genuine Octopus Intelligent tariff, fetch_octopus_rates() can receive a dispatch period whose rate is already the Intelligent low rate before rate_add_io_slots() ever runs - rate_replicate() only gap-fills minutes with no real fetched value, so it never touches one that's already there. When rate_add_io_slots() then decides a slot isn't needed (or the daily cap is already reached), `needed=False`/cap-exceeded only prevented adding a *new* low rate; it left whatever was already in rates[] untouched, so an already-discounted fetched rate stayed live even though the slot was just rejected. Confirmed live: a 05:30-06:00 dispatch Predbat correctly determined the car no longer needs, where Octopus's own event data already carried 3.99p (matching the dispatch event's own max_rate) instead of the tariff's ordinary 29.3p out-of-window rate. Now restores rates[minute] to rate_max_base for every minute of a rejected 30-min block (both branches of the existing minute%30==0 split, not just the slot-start minute), and clears self.io_adjusted for the same range - minute_data() (utils.py) sets it for every minute in an adjusted block, not just the first, so a partial clear would leave the plan believing an already-restored minute was still IOG-adjusted. New regression test simulates the exact scenario (a rejected slot with its rates[] pre-seeded as fetch_octopus_rates() would leave it), confirmed to fail without this fix and pass with it. Existing 23 rate_add_io_slots scenarios pass unchanged - their reject-path expectations already happened to equal rate_max_base in that fixture, so this only changes behaviour when rates[] holds something other than the baseline.
|
Pushed the restoration fix discussed above. A rejected slot (not needed, or the daily cap already reached) now restores New regression test simulates the exact scenario (rejected slot with Full suite + pre-commit clean. |
|
Thanks, this now addresses the live failure mode I reported: restoring every minute to rate_max_base and clearing every corresponding io_adjusted marker is exactly what the pre-adjusted 3.99p input requires. I noticed one possible side effect in the revised rejection path. The same destructive restoration also runs when the slot is rejected only because octopus_slot_max has been reached: if needed and slots_per_day[day_offset] < octopus_slot_max: That changes the previous cap behavior even when octopus_intelligent_limit_future_slots is Off. It could also overwrite a legitimate tariff rate, including a guaranteed fixed-window rate, despite the PR intending 23:30–05:30 to remain unaffected. Would it be safer to perform the active restore and io_adjusted clear only when needed is false? A slot rejected solely by the existing daily cap could retain the previous behavior unless its undiscounted source rate is known. A regression test with the feature switch Off and octopus_slot_max already reached would confirm whether this behavior is intentional. A capped slot within 23:30–05:30 may also be worth covering. I very much appreciate your work on this. |
…mpt zero-kWh dispatches from the daily cap Two follow-ups from PR review on springfall2008#4483: 1. The active rate/io_adjusted restore added in the previous commit was reached by *any* rejection in rate_add_io_slots(), including a slot rejected purely because octopus_slot_max was already reached (needed stays True) - a pre-existing, unrelated mechanism that fires even with octopus_intelligent_limit_future_slots Off. That could overwrite a legitimate live rate Predbat was simply choosing not to count against its own daily budget, not one Octopus is known to have rescinded. The restore now only fires when needed is False (Speshman, PR springfall2008#4483 review). 2. A zero-kWh dispatch entry (e.g. a plug-independent SMART grid-flex event that delivers no energy to the car) is a real tariff discount, but not a car-charging dispatch, so by default it no longer competes for the octopus_slot_max budget or the springfall2008#4482 "does the car still need this" check - both model car-dispatch behaviour specifically. Gated behind a new switch, octopus_slot_count_zero_kwh (default Off = don't count them), so anyone who wants the old counting behaviour back can restore it. Adds 5 regression tests (25-29) covering both, including the two review specifically asked for (cap-only rejection with the feature Off, and a capped slot inside the fixed 23:30-05:30 window). All fail on the pre-fix code, pass with it. Full suite + pre-commit clean.
|
Pushed 3b1feda addressing both points. The restore scope: agreed, and confirmed the exact mechanism you flagged - Second thing, flagged separately by Rik while reviewing this: a zero-kWh dispatch entry (a plug-independent SMART grid-flex event Octopus schedules that delivers no energy to the car) was still consuming a slot of the 12-slot daily budget under the old counting, which could crowd out real charging dispatches later in the day even on a day the car never got close to 12 real slots. Added Thanks for reading the actual diff rather than just eyeballing the PR description - the cap-rejection path especially wasn't something we'd have caught without it. |
# Conflicts: # apps/predbat/tests/test_fetch_config_options.py
main independently added its own Test 14 (num_cars clamp) and Test 15 (get_car_charging_planned) since this branch was cut, colliding with this PR's own Test 14. Also moved the octopus_intelligent_limit_future_slots warning test to run before the mock get_arg/args restore, since it needs the mocks still active.
|
If you need anything else from me please just ask. I'm still quite new to all this so on a vertical learning curve 😁 |
|
Thanks @Speshman - I'm pretty new here myself. Subject to review from @springfall2008 now I think |
Fixes #4482.
Summary
car_charging_soc/car_charging_limit.load_octopus_slots()already capscar_charging_slots[car_n]at the car's real remaining requirement whenoctopus_intelligent_consider_fullis on, zeroing the kwh of any slot beyond that - butrate_add_io_slots()(which decides which dispatch slots the house battery can rely on as a cheap rate) never looked at that, so it kept treating every granted slot as low rate regardless of whether the car's own plan still needed it.octopus_intelligent_limit_future_slots(expert mode, default Off, requiresoctopus_intelligent_consider_fullto have any effect - warns at startup otherwise): a future out-of-window dispatch slot only counts as low rate for the house battery while it falls within a 30-min blockcar_charging_slotsstill lists a positive kwh for. A slot already underway or completed is trusted regardless (only future need is gated), and the fixed 23:30-05:30 window is never affected, since it's guaranteed cheap by the tariff itself.Test plan
rate_add_io_slotsscenarios: partial-need-vs-full settlement period, car already full, current/completed dispatch exempted, switch off restores old behaviour, fixed window unaffectedfetch_config_optionswarning test for theconsider_fulldependency./run_all --quick./run_pre_commit