Repository navigation
octopus_slot_max is derived per account but enforced per car #5215
Description
(Written by Claude, posted by @chalfontchubby.)
Found while reviewing #5110. Not caused by that PR and not fixed there - raising separately because fixing it changes behaviour for every multi-car IOG install.
The bug
octopus_slot_max is derived per account but enforced per car.
get_octopus_slot_max() (octopus.py:3232) takes no car_n. It resolves the cap from the account's import tariff code via has_six_hour_cap() (octopus.py:1698), which matches IOG-SMB - a property of the tariff, not of a vehicle. One value for the whole install.
But slots_per_day is a local in rate_add_io_slots() (octopus.py:3897), and fetch.py calls that function once per car. The counter therefore resets for every car:
for car_n in range(self.num_cars):
import_rates = self.rate_add_io_slots(car_n, import_rates, self.octopus_slots[car_n])
So with the IOG-SMB cap of 12 and two IOG-enrolled cars, Predbat honours up to 24 cheap slots against an account allowance of 12. It is never on self (grep -c "self.slots_per_day" → 0), so nothing carries the count between cars.
load_octopus_slots() has its own slots_per_day (octopus.py:3555) with the same shape.
Consequence
Predbat prices up to octopus_slot_max × num_cars half-hours at the off-peak rate per day. On an install where Octopus really does cap at 6 hours per account, the slots beyond the allowance will be billed at the day rate while Predbat plans against the cheap one - so it charges the battery in windows that turn out expensive. Single-car installs are unaffected.
Why this is not just a docs wording issue
docs/car-charging.md:302 currently says:
Note: The
octopus_slot_maxlimit applies per-car, so with two cars on IOG each car is subject to its own slot-count cap.
That documents the current behaviour, but the cap it describes is derived from an account-level tariff code, so per-car enforcement cannot be what was intended. Worth noting the Octopus app shows the charge cap on the same screen as per-car preferences, which may be where the per-car reading came from - but a setting rendered in a per-car context is not evidence that the allowance itself is per-car.
Suggested fix
Move slots_per_day onto self, reset once per cycle in fetch.py beside the other per-cycle accumulators (trusted_dynamic_minutes, io_adjusted), so the budget is shared across the per-car loop. Structurally the same as protected_dispatch_minutes in #5110.
This needs a maintainer decision before anyone implements it, because it tightens behaviour: an install currently getting 12 slots per car would drop to 12 shared. If per-car budgets are actually wanted, the alternative is to make octopus_slot_max an indexed (per-car) argument - it is a plain {"type": "integer"} in config.py:2799 today and is read without index=, so per-car configuration does not currently exist either way.
I have not changed any of this. Happy to implement whichever way you want it.
Activity
docs(car-charging): octopus_slot_max is per account, not per car
springfall2008 commentedon Sep 24, 2026
🤖 Automated first-pass triage — a maintainer will review before any labels/priority are acted on.
Classification: bug (matches the existing label) · Priority: priority_medium
I verified every code claim in this report against current
main(130596bc,v9.2.0-18-g130596bc). All of them hold; the line numbers in the body drift slightly (~3-40 lines, the report was written against a tree with a few extra lines in this area), so here aremain's numbers:get_octopus_slot_max()—octopus.py:3190— takes nocar_n. It resolves the cap from the account's import tariff code viahas_six_hour_cap()(octopus.py:1698, matchesIOG-SMB→ 12) or from the single apps.yaml value. One value for the install.rate_add_io_slots()—octopus.py:3588—slots_per_day = {}is a local (octopus.py:3598), andfetch.py:1242-1243calls the function once per car, so the counter resets for every car.load_octopus_slots()—octopus.py:3430— has its own localslots_per_day(octopus.py:3441) and is likewise called per car (fetch.py:1588). Both cap counters double up on multi-car installs.config.py:2796—"octopus_slot_max": {"type": "integer"}, read withoutindex=atoctopus.py:3199— so per-car configuration genuinely does not exist either way, as reported.docs/car-charging.md:302documents the per-car enforcement exactly as quoted.grepconfirms noself.slots_per_dayanywhere, andgit log -S slots_per_dayshows the last touches on this area were the cap modelling and the original Add Octopus Intelligent 6-hour daily slot limit #3022 limit — nothing onmainshares the budget. PR #5110 (still open) touches these functions but gates trust, not the cap budget — consistent with the report's framing.I did not run a test: the existing
test_rate_add_io_slots/test_octopus_slotsmodules encode the current per-car behaviour, so a run can only restate what static reading already settles.The open question is exactly the one the report flags. The code is internally inconsistent whichever semantics Octopus actually applies — the cap is derived from an account-level property (the tariff code) but enforced N times. One data point for the maintainer's decision: in #4830 the Octopus blog wording quoted there says "Your car allowance: Your car gets up to 6 hours of off-peak charging per day" — which reads per-car, while the code's tariff-code derivation reads per-account. Also worth noting the exposure is invisible outside IOG-SMB: uncapped tariffs default to 48, so only capped multi-car installs can see the doubling.
Both fix options in the report (shared budget on
selfreset per cycle, vs. an indexed per-car argument) remain valid; which one is right depends on the semantics the maintainer confirms. Not a duplicate — #4950 (closed) fixed a different aspect of this counter (the midday day-boundary keying), and #4830 is the question thread where the cap semantics first surfaced, but neither addresses the per-car reset.