Skip to content

Octopus: Intelligent devices stay cached and wired to the car slots after the account leaves an Intelligent tariff #5412

Closed

Description

@springfall2008

Summary

When an account moves off an Octopus Intelligent tariff, OctopusAPI keeps its last known Intelligent devices for good. They are never refreshed or pruned again, they are saved back to the cache, and the car slots stay wired to their dispatch entities. Predbat goes on treating the car as an Octopus Intelligent car on a tariff that has no dispatches.

Found while reviewing #5405. Not reproduced on a live install: this is from reading the code on main at 4fb634b, and the cached-device half is exercised by a test in #5405.

What happens

async_update_intelligent_devices() returns before doing anything when the import tariff is not Intelligent:

import_tariff = self.tariffs.get("import", {})
tariffCode = import_tariff.get("tariffCode", "")
if not self.is_intelligent_go_tariff(tariffCode):
    return

So from the moment the tariff changes:

  1. self.intelligent_devices is frozen as it was on the last Intelligent poll. The pruning that removes a device Octopus no longer reports as live sits below that early return, so it never runs.
  2. save_octopus_cache() writes the frozen devices back to the cache every cycle, and load_octopus_cache() restores them on restart. A restart does not clear them.
  3. async_intelligent_update_sensor() keeps publishing each device's dispatch entity, with the planned and completed dispatches it had at the time of the change.
  4. automatic_config() wires octopus_intelligent_slot, octopus_ready_time and octopus_charge_limit to those entities, on the first run after every restart and whenever it re-runs. Nothing in it looks at the tariff.

Effect

  • octopus_intelligent_slot stays set, so fetch_sensor_data_car_planning() takes the Octopus Intelligent branch for that car. With no live dispatches the car gets no plan, and plan_car_charging() is never called for it.
  • The frozen dispatches are still fed to rate_add_io_slots(), which looks back up to 96 hours, so for the first days after the change past minutes can still be stamped at the off-peak rate.
  • num_cars is held at the old device count.
  • With an Ohme charger: the Ohme component sees octopus_intelligent_slot pointing at the Octopus entity and stands down, so the "Ohme's own schedule is the car plan" mode from feat(ohme): take the car plan from Ohme's own schedule when Predbat is not controlling the charger #5401 never starts for someone who has left Intelligent.

A user who sets octopus_intelligent_slot themselves, or runs with octopus_automatic: false, is not affected by the wiring half.

Proposal

In async_update_intelligent_devices(), when the import tariff code is known and is not Intelligent, clear self.intelligent_devices instead of returning with it untouched. The existing device-set check in run() then sees the set change and re-runs automatic_config(), which clears the car slot wiring it made (after #5405 it only clears wiring that is its own).

Two cases to keep as they are:

  • Tariff not known (tariffs empty, between fetches or after a failed account fetch): keep the devices, as now. Dropping an Intelligent car's dispatches over a gap in the account data would be worse than the bug.
  • Kraken / E.ON / EDF SmartFlex: check whether those tariffs pass is_intelligent_go_tariff() before relying on it here, since the same early return already applies to them.

Related

Activity

  1. springfall2008 commented on Oct 5, 2026

    @springfall2008
    OwnerAuthor

    Automated first-pass triage — a maintainer will review before any action is taken. No log/debug files needed: the cause is a code-read finding, so it was verified against current main directly. Everything below was confirmed on 7e6cd27a (v9.3.5), probe-verified with a temporary local test (not committed), not live-tested.

    Mechanism confirmed end-to-end. async_update_intelligent_devices() early-returns on a non-Intelligent import tariff (apps/predbat/octopus.py:1002), and the probe shows the poll then never even reaches the dispatch API, so the pruning at octopus.py:1028 below the return never runs: self.intelligent_devices stays exactly as it was when the tariff last passed the gate. automatic_config() is tariff-blind (octopus.py:1417 wires purely from the device dict), so the car-slot wiring written while on IOG persists after the change. save_octopus_cache()/load_octopus_cache() round-trip the device dict (octopus.py:885/866) — in the probe a fresh instance restored the frozen device from the cache, so a restart does not clear it. Downstream: fetch_sensor_data_car_planning() takes the Octopus Intelligent branch for that car (fetch.py:1407) and never calls plan_car_charging(); self.octopus_slots is rebuilt every cycle from the frozen entities' attributes (fetch.py:984, 1512-1516) and rate_add_io_slots() stamps completed dispatches back up to 96 hours (octopus.py:3795). The Ohme half checks out too: octopus_intelligent_wanted() re-evaluates the tariff on every poll and flips to false once the new code is known (ohme.py:509-515), but charger_slots_wanted() then refuses to take the slots because octopus_intelligent_slot is still set to the frozen Octopus entity (ohme.py:584-594) — so the #5401 charger-schedule mode stays blocked for as long as the freeze lasts.

    On the caution — Kraken / E.ON / EDF SmartFlex: no additional tariff family is at risk. A tariff code in this poll comes only from Octopus's own account agreements (octopus.py:954, 972); the E.ON/EDF SmartFlex devices are the separate KrakenAPI component, whose discovery is not tariff-gated at all and prunes non-live devices on every poll (kraken.py:1162-1201). Octopus's own non-Intelligent products (GO-, COSY, FLUX, fixes, Agile) all correctly fail is_intelligent_go_tariff() since only IOG-/INTELLI-` products carry Intelligent dispatches.

    On "tariff not known → keep": agreed, with one precision worth baking into the fix. async_find_tariffs() can write tariffCode: None into a live import dict (an active agreement with no tariff object, octopus.py:954 → 972), and import_tariff.get("tariffCode", "") returns that None rather than the default — is_intelligent_go_tariff() is false for it either way. So the gate should be truthy code and not Intelligent → clear; falsy or missing → keep, or an agreement-data gap could wipe an IOG account's devices over one bad poll.

    Proposal mechanics verified. Clearing the devices trips run()'s device-set check (octopus.py:761) and automatic_config() writes empty lists for all three slot args ("No active intelligent devices remain, cleared the car slot wiring"). The stamping then stops that same cycle, not over days: self.octopus_slots is rebuilt each cycle from the now-unset wiring (fetch.py:984). The empty-devices branch of async_intelligent_update_sensor() also trips intelligent_dispatch_replan_check([]), whose signature change requests a replan (octopus.py:2618, 2663-2690). num_cars is not lowered by automatic_config() in any case (octopus.py:1453-1455), so the held count behaves like the #4648 device-deregistration case rather than being a new side effect of the fix.

    One residual hole worth a decision. tariffs["import"] staying absent is not the same as "between fetches": tariffs is rebuilt from cached account_data every 30-minute cycle and a failed account fetch keeps the old data (octopus.py:2698-2700), so persistent absence means the account genuinely has no active import agreement. The keep-as-was wording leaves that case frozen forever by the same mechanism — worth deciding whether "no import agreement while devices remain" should also clear.

    One caveat correction (probe-verified). automatic_config() overwrites a user's own apps.yaml octopus_intelligent_slot while the frozen device set is non-empty — set_arg() writes unconditionally (userinterface.py:195-215) — so "a user who sets octopus_intelligent_slot themselves is not affected" holds only with octopus_automatic: false; under automatic mode the frozen wiring beats their manual entry on the first run after every restart. Does not change the proposal.

    Tests: octopus_day_night_rates passes on main and pins only the opposite direction (Test 10: an IOG-only code must proceed to the poll); nothing currently pins keep-forever on a non-Intelligent code, so the proposal adds a case rather than flipping a pinned one.

    Type: bug · component Octopus · priority_medium — affects automatic-config users after leaving an Intelligent tariff (car unplanned, stale cheap-rate stamps, and an Ohme charger-schedule takeover that never starts). Workaround exists (octopus_automatic: false plus manual wiring) but is an expert-level ask.

    Related: #4648 is the device-set trigger of the same stale wiring (fixed by the run() check that makes this proposal's fix path work); #5401/#5402/#5405 are the Ohme side of who supplies the car slots.

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