Repository navigation
Octopus: Intelligent devices stay cached and wired to the car slots after the account leaves an Intelligent tariff #5412
Description
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):
returnSo from the moment the tariff changes:
self.intelligent_devicesis 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.save_octopus_cache()writes the frozen devices back to the cache every cycle, andload_octopus_cache()restores them on restart. A restart does not clear them.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.automatic_config()wiresoctopus_intelligent_slot,octopus_ready_timeandoctopus_charge_limitto those entities, on the first run after every restart and whenever it re-runs. Nothing in it looks at the tariff.
Effect
octopus_intelligent_slotstays set, sofetch_sensor_data_car_planning()takes the Octopus Intelligent branch for that car. With no live dispatches the car gets no plan, andplan_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_carsis held at the old device count.- With an Ohme charger: the Ohme component sees
octopus_intelligent_slotpointing 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 (
tariffsempty, 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
- Octopus Intelligent: car↔device linkage in automatic_config() goes stale when live device set changes #4648: the car slot wiring going stale when the live device set changes (fixed). This is the same wiring going stale when the tariff changes instead.
- feat(ohme): take the car plan from Ohme's own schedule when Predbat is not controlling the charger #5401, fix(ohme): leave the Intelligent car slots to Octopus when the Intelligent device is the car #5405: the Ohme side of who supplies the car slots.
Activity
springfall2008 commentedon Oct 5, 2026
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 atoctopus.py:1028below the return never runs:self.intelligent_devicesstays exactly as it was when the tariff last passed the gate.automatic_config()is tariff-blind (octopus.py:1417wires 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 callsplan_car_charging();self.octopus_slotsis rebuilt every cycle from the frozen entities' attributes (fetch.py:984,1512-1516) andrate_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), butcharger_slots_wanted()then refuses to take the slots becauseoctopus_intelligent_slotis 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 separateKrakenAPIcomponent, 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 failis_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 writetariffCode: Noneinto a liveimportdict (an active agreement with no tariff object,octopus.py:954→972), andimport_tariff.get("tariffCode", "")returns thatNonerather 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) andautomatic_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_slotsis rebuilt each cycle from the now-unset wiring (fetch.py:984). The empty-devices branch ofasync_intelligent_update_sensor()also tripsintelligent_dispatch_replan_check([]), whose signature change requests a replan (octopus.py:2618,2663-2690).num_carsis not lowered byautomatic_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":tariffsis rebuilt from cachedaccount_dataevery 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.yamloctopus_intelligent_slotwhile the frozen device set is non-empty —set_arg()writes unconditionally (userinterface.py:195-215) — so "a user who setsoctopus_intelligent_slotthemselves is not affected" holds only withoctopus_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_ratespasses 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: falseplus 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.