Skip to content

Octopus Intelligent: low-load test cancels a genuine dispatch while the car is charging in view of the CT #5461

Description

@springfall2008

Summary: On Octopus Intelligent, the low-load test in the slot confirmation can cancel a genuine dispatch while the car is charging at full power and is visible in the house load. A single 5-minute load figure under 0.9 × car_charging_threshold is enough. Once the slots are cancelled the battery hold is released and the battery discharges into the car. The way dynamic load judges "low" probably needs adjusting to allow for this.

This is not the charger-outside-the-CT case (#5317 / #5318). Here car_energy_reported_load is correctly On and the car shows in the load.

What happened

One install on v9.3.5: Fox ESS (cloud), 16.9 kWh battery at 8 kW, a 7.4 kW car charge, no car_charging_now sensor, car_charging_threshold at the default 6.0 kW. Times are BST, Thursday 8 October 2026 into Friday 9 October.

Time Log / reading Effect
20:35–20:45 In a dispatch; battery grid-charging at 6–7.8 kW next to the car. Load reads 5.8–6.1 kW
20:40:08 car 0 is in a dispatch but not charging, cancelling its slots Battery discharges into the car, 20:45–20:50 (about 0.6 kWh)
20:50:07 car 0 slots resumed
22:40:08 cancelling its slots again. Inverter load at that moment: 7410 W No drain (battery already at reserve)
22:45:09 slots resumed
23:50:09 cancelling its slots - keeping the dispatch rate until 00:00 Battery discharges into the car, 00:05–00:15 (about 1.2 kWh)
00:10:08 slots resumed

Octopus was reporting the dispatch correctly throughout (SMART_CONTROL_IN_PROGRESS, planned dispatch present).

The line that shows the cause, at 22:40:13:

Inverter 0 SoC: 2.37kWh 14%, ... grid power -7410W, load power 7410W, PV Power 0W
Dynamic load last period 4.80kW, status low, threshold_battery 8.0kWh, threshold_car 6.0kWh,

Why it reads "low"

dynamic_load_classify() (apps/predbat/plan.py) returns "low" when load_last_period is under both 0.9 × battery rate and 0.9 × car_charging_threshold. With the defaults that is 5.4 kW. dynamic_load_car_evidence() then cancels after DYNAMIC_LOAD_CAR_LOAD_MINUTES = 5, which is one period.

Two things pushed a 7.4 kW car under 5.4 kW:

  1. Ramp-up and telemetry lag. load_last_period is the load energy over the last PREDICT_STEP minutes. The dispatch started at 22:30 and the load only reached 7 kW at about 22:35, so the 22:35–22:40 figure came out at 4.80 kW. A cloud inverter whose readings arrive a few minutes late makes this worse. The existing guard only requires the 5-minute window to lie inside the dispatch; it does not allow for the car starting late or the data arriving late.
  2. Load under-reads while the battery charges. On this Fox system the reported load drops from about 7.5 kW to 5.6–6.1 kW whenever the battery grid-charges at 7.8 kW alongside the car. That sits right on the 5.4 kW line. I have the "last period" log value only for the 22:40 cancel; for 20:40 and 23:50 this is my reading of the power data, not a logged figure.

So a 7 kW charger has very little margin against the default threshold, and the test fires exactly when Predbat itself starts charging the battery in the dispatch.

Suggestions

Any of these would have avoided it; they are not exclusive.

  • Compare against the baseline as well as the threshold. 4.8 kW is not a car at full rate, but it is about 4 kW above this house's load just before the dispatch (0.6–0.7 kW). "Low" could mean "close to what the load was before the dispatch / what was forecast without the car", not just "under 0.9 × threshold".
  • Require more than one low period before cancelling, at least when the load is well above baseline, or lengthen the grace for the load test on installs with slow telemetry. One 5-minute sample is a thin basis for releasing the battery hold.
  • Use the car's own rate. The dispatch gives the charge rate (7.4 kW here). A cut-off at a fraction of that rate (say half) would fit a 3.6 kW and a 7.4 kW car alike, where a fixed 6 kW setting fits neither well.
  • Use grid power as a cross-check. At every false cancel the grid import was 7.4–13.5 kW. Import well above the forecast house load plus any planned battery charge is evidence the car is charging, and it does not depend on how the inverter derives "load".
  • Ignore periods when the battery is charging hard, or correct for them, if load derived during a grid charge is known to be unreliable on some inverters.

Workaround in use

car_charging_threshold lowered from 6.0 to 4.0 kW (its minimum) on the affected install, which moves the cut-off to 3.6 kW. It works against this failure but it is the wrong tool: the same setting decides what is stripped from load history as car charging, so lowering it removes more genuine household load from the forecast. The other option is turning octopus_intelligent_dynamic off, which loses the confirmation altogether.

Related: #5318 (other evidence sources for the same check), #5229 / #5248 (where the confirmation came in).

Activity

  1. springfall2008 commented on Oct 9, 2026

    @springfall2008
    OwnerAuthor

    🤖 Automated first-pass triage (a maintainer will review before any action is taken).

    Classification: bug, priority_medium. Affected cohort: Octopus Intelligent installs on defaults — octopus_intelligent_dynamic (default on) with car_energy_reported_load (default on), no car_charging_now sensor, and a car drawing ~7 kW — where a lagged or battery-charge-dipped 5-minute load figure drops under 0.9 × car_charging_threshold (5.4 kW at the 6.0 kW default) while a genuine dispatch is running. Workarounds exist but each gives something up (see the end), so medium rather than low.

    Checked against main (v9.3.6-6-g42da8559): the classify/evidence code is identical on the v9.3.5 this report ran — the only plan.py commits between the two versions are #5120 and #5401, elsewhere in the file — so nothing here is version-specific.

    Verified:

    • The quoted log line is dynamic_load()'s (apps/predbat/plan.py:250); behind it dynamic_load_classify() (plan.py:380) classifies "low" exactly as the report describes: load_last_period under both 0.9 × battery_rate_max_discharge (7.2 kW here) and 0.9 × car_charging_threshold (5.4 kW here). With no car_charging_now sensor the load test (dynamic_load_car_evidence(), plan.py:413-420) turns "low" into not-charging evidence, gated only on the whole trailing 5-minute window lying inside the dispatch (plan.py:418) — so the dispatch's very first (ramp-up) window is eligible evidence, and the grace is DYNAMIC_LOAD_CAR_LOAD_MINUTES = 5 minutes (const.py:35), timed on the 5-minute cycle grid (the 15-second poll deliberately does not run the load test — plan.py:805).
    • One precision on the grace: a single low figure starts the grace clock (plan.py:574) but does not itself cancel — the cancel fires when the next cycle, 5 minutes later, is also low (plan.py:575-576). So a cancellation needs two consecutive low 5-minute averages, and because the ramp-up window is eligible evidence, one lagged or under-read figure rides it into a cancel. Reading the reported 22:30 dispatch backwards from the 22:40:08 cancel: the [22:30–22:35) window must also have read low (the ramp-up), and the logged 4.80 kW [22:35–22:40) window cancelled it. The same-cycle inverter snapshot (load power 7410W) against the 4.80 kW average is the lag in one line — the instantaneous load had already reached 7.4 kW while the trailing average sat 2.6 kW lower.
    • The readings of 5.8–6.1 kW quoted for the battery-charge under-read sit just above the 5.4 cut-off and would classify as baseline (slot stays trusted; the 5.4–6.0 strand never satisfies the cheap-rate confirmation either, which needs the full threshold — plan.py:565). So the 20:40 and 23:50 cancels imply those specific windows averaged under 5.4 kW, i.e. dips below the line rather than a sustained under-read. The Dynamic load last period line prints every cycle (plan.py:250), so a retained predbat.log covering the evening would pin those two figures directly, if wanted for a fix's fixtures.
    • The dynamic_load_car test module passes on main; it encodes the current designed threshold behaviour. The misfire needs real telemetry — ramp-up, lag, and the under-read while the battery grid-charges (the latter the reporter's measured Fox behaviour, not code-verifiable from here) — which the mocks do not model.

    Root cause: the design of the low test itself — fixed 0.9× fractions of two thresholds evaluated on the single trailing 5-minute average, a grace of exactly one period, and no baseline, battery-charging or grid-import cross-check. All five directions in the suggestion list are still open against current main. #5318 (open, enhancement) is the adjacent request to use the charger's own energy sensor as independent evidence, which would also immunise this path against the load under-read.

    Workaround premise, refined: the car_charging_threshold history-stripping heuristic only runs when there is no real car_charging_energy sensor — with one configured, load history is subtracted from the sensor precisely and the threshold heuristic is skipped (fetch.py:382-389, GH#4717; load_ml_component.py:75/360 is the same shape). So on an install that has a working car_charging_energy sensor, lowering the threshold to 4.0 kW does not remove more household load from the forecast; on installs without one, the coupling the report worries about stands.

    Related: #5318, PRs #5229/#5248 (where the confirmation came in), #5317 (the charger-outside-the-CT shape the report already distinguishes), #5390 (the sensor-path cancel/resume flapping sibling). No duplicate found — none of those covers this cause.

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions