Skip to content

octopus_slot_max is derived per account but enforced per car #5215

Open

Description

@chalfontchubby

(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_max limit 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

  1. springfall2008 commented on Sep 24, 2026

    @springfall2008
    Owner

    🤖 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 are main's numbers:

    • get_octopus_slot_max() — octopus.py:3190 — takes no car_n. It resolves the cap from the account's import tariff code via has_six_hour_cap() (octopus.py:1698, matches IOG-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), and fetch.py:1242-1243 calls the function once per car, so the counter resets for every car.
    • load_octopus_slots() — octopus.py:3430 — has its own local slots_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 without index= at octopus.py:3199 — so per-car configuration genuinely does not exist either way, as reported.
    • docs/car-charging.md:302 documents the per-car enforcement exactly as quoted.
    • grep confirms no self.slots_per_day anywhere, and git log -S slots_per_day shows the last touches on this area were the cap modelling and the original Add Octopus Intelligent 6-hour daily slot limit #3022 limit — nothing on main shares 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_slots modules 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 self reset 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions