Skip to content

fix(octopus): don't commit the house battery to an unconfirmed Intell…#4270

Open
chalfontchubby wants to merge 4 commits into
springfall2008:mainfrom
chalfontchubby:fix/iog-slot-rescind-risk
Open

fix(octopus): don't commit the house battery to an unconfirmed Intell…#4270
chalfontchubby wants to merge 4 commits into
springfall2008:mainfrom
chalfontchubby:fix/iog-slot-rescind-risk

Conversation

@chalfontchubby

@chalfontchubby chalfontchubby commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

🤖 Drafted with Claude Code assistance.

Related to #4248 and the discussion there (in particular @jHamshaw's report of the house battery discharging into a slot that later gets rescinded).

The problem

Octopus Intelligent Go can issue a daytime "smart dispatch" slot (outside the fixed 23:30-05:30 off-peak window). That slot is still Octopus's own provisional/revisable plan - if the car never actually draws power during it, Octopus retroactively rescinds the cheap rate. rate_add_io_slots previously wrote the low rate into rate_import for every dispatch slot, planned or completed, with no distinction - so the house battery could commit to charging on a rate that later reverts to full price.

The fix

Adds a new expert-mode switch, octopus_intelligent_confirm_slots (default Off): when enabled, an out-of-window slot is only treated as low-rate once it's confirmed - either as a completed (metered) dispatch, or via car_charging_now confirming real-time draw. The fixed overnight window is always trusted regardless, since it's guaranteed cheap by the tariff itself, not the dispatch mechanism.

This composes cleanly with the existing octopus_slot_max 6-hour cap (octopus.py:2649, if confirmed and slots_per_day[day_offset] < octopus_slot_max:) - both are independent, ANDed conditions, no conflict.

Why default Off, with a warning rather than defaulting On: investigation showed car_charging_now is really the only confirmation signal fast enough to matter for a live slot - it ships commented out by default, existing docs discourage setting it for most users, and Octopus's "completed dispatch" confirmation may not arrive until after the slot has ended. Given how sweeping the consequence is without car_charging_now (daytime slots become permanently untrusted, not just occasionally), this seemed too large a behaviour change to ship as a silent default-On. Instead: default Off, plus a startup warning if the switch is on, Octopus Intelligent Charging is enabled, and car_charging_now isn't set - so turning it on without the signal it needs doesn't fail silently.

Why not infer it from load instead, and skip needing car_charging_now at all? Predbat's dynamic_load mechanism has a car-slot cancellation check that looks like a candidate, but it isn't one on closer inspection: load_car_slot isn't derived from load telemetry, it's just "are we inside a car_charging_slots window" - and car_charging_slots is itself built directly from the same provisional Octopus dispatch data being confirmed here, so using it as confirmation would be circular. The only thing it does in response to observed load is one-directional and negative (it can cancel an assumed slot if load stays surprisingly low; there's no symmetric path that positively confirms one), and it's gated behind metric_dynamic_load_adjust, which defaults Off anyway.

Beyond the existing mechanism, a purpose-built load-spike heuristic wouldn't fare much better here. A single-phase EV charger's draw overlaps in both magnitude and duration with common household loads (oven, electric shower, immersion heater, tumble dryer) - disambiguating "the car is charging" from "someone's cooking" out of one aggregate whole-house signal is a real signal-disaggregation problem (NILM), not something a threshold check reliably solves. It's also worst exactly when this fix matters most - daytime, when other household loads are most active. And real charging current isn't flat: cold-battery preconditioning, BMS tapering near full SOC, and load-balancing EVSEs can all put genuine charging well under a fixed threshold, so no single cutoff avoids both false positives (from other appliances) and false negatives (from a slow-charging car) at once.

Also included

  • Docs (docs/car-charging.md) updated to state plainly that car_charging_now is effectively required for this protection to do anything on daytime slots, regardless of car_energy_reported_load.
  • A worked car_charging_now example (including that it can point at a charger-level sensor rather than the car's own API - useful for chargers Octopus controls directly, e.g. Hypervolt) added to the main apps.yaml template and all 13 inverter templates carrying the same block.
  • Full test coverage for the new confirmation logic (test_rate_add_io_slots.py) and the startup warning (test_fetch_config_options.py).

Verification

  • Full local suite passes (591/591 non-flaky tests; the one known-flaky debug_cases case is unrelated, see prior PR discussion).
  • run_pre_commit passes cleanly.
  • Manually verified octopus_slot_max and octopus_intelligent_confirm_slots compose as independent AND conditions with no interference.

…igent Go daytime slot

Octopus Intelligent Go can issue a daytime "smart dispatch" slot outside the
fixed 23:30-05:30 off-peak window. That slot is still Octopus's own
provisional/revisable plan - if the car never actually draws power during it,
Octopus retroactively rescinds the cheap rate. rate_add_io_slots previously
wrote the low rate into rate_import for every dispatch slot, planned or
completed, with no distinction - so the battery could be committed to
charging on a rate that later reverts to full price. See springfall2008#4248.

Adds a new expert-mode switch, octopus_intelligent_confirm_slots (default
On): an out-of-window slot is only treated as low-rate once confirmed -
either as a completed (metered) dispatch, or via car_charging_now confirming
real-time draw. The fixed overnight window is always trusted regardless of
provenance, since it's guaranteed cheap by the tariff itself.

Threads a new `provisional` tag from where planned/completed dispatches are
merged in fetch.py through decode_octopus_slot into rate_add_io_slots, which
now processes confirmed slots before still-provisional ones (stable sort) so
a later car_charging_now confirmation can't be silently dropped by the
existing per-minute dedup claiming the minute first.

Verified no downstream window-merging mechanism (combine_charge_slots,
execute.py's contiguous-window combine, plan.py's discard_unused_charge_slots)
can bridge an unconfirmed minute into a charge window - all three only ever
merge windows/minutes that already independently qualified.
@springfall2008

Copy link
Copy Markdown
Owner

I guess the only issue with this is many people don't have car_charging_now set up, I wonder if we could do it based on load?

@chalfontchubby

chalfontchubby commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

If car load is included in predbats visible that might be viable. But it would involve some assumptions about the load on the house that might be prone to risk. Car_charging_now is kinda useful in this case - don't know if there is something (quickly) visible in the octopus integration that could confirm the slot has been used.

For all of this, I don't want to get in the way, and only want to help if it's useful. Happy to be "managed" or told to back off as needs be.

…s missing

Investigation showed car_charging_now is the only real-time confirmation
signal available for a daytime IOG dispatch slot - it ships commented out by
default (apps/predbat/config/apps.yaml), existing docs actively discourage
setting it for most users, and Octopus's "completed dispatch" confirmation
may not arrive until after the slot has already ended. The other candidate
fallback, dynamic_load's car-slot cancellation, turns out to be derived from
the same provisional octopus_slots data being confirmed (car_charging_slots
is built directly from it via load_octopus_slots), so it can't independently
verify anything - it's also off by default (metric_dynamic_load_adjust).

Given how sweeping the consequence is without car_charging_now - daytime
slots become permanently untrusted, not just occasionally - default
octopus_intelligent_confirm_slots to Off rather than On, so the behaviour
change only applies when a user has actually opted in.

Adds a startup warning (fetch_config_options) when the switch is on,
Octopus Intelligent Charging is enabled, and car_charging_now isn't set,
so turning this on without the signal it needs doesn't fail silently.
Updates docs/car-charging.md and the rate_add_io_slots docstring to state
this plainly, and adds a worked car_charging_now example (including a
charger-level sensor, not just a car API) to the main apps.yaml template
and all 13 inverter templates that carry the same block.
…unreliable

Expands on why car_charging_now has no working fallback: dynamic_load's
car-slot cancellation isn't load-based confirmation at all (it just checks
car_charging_slots, which is itself built from the same provisional dispatch
data), is one-directional (can only cancel, never confirm), and is gated
behind metric_dynamic_load_adjust which defaults Off.

Also explains why a purpose-built load-spike heuristic wouldn't fare much
better: an EV charger's draw overlaps in both magnitude and duration with
common appliances (oven, shower, dryer), disambiguating them from one
aggregate whole-house load signal is a real signal-disaggregation problem
(NILM), and it's worst exactly during daytime - when this protection
matters most and other household loads are most active. Real charging
current also isn't flat (preconditioning, BMS tapering, load-balancing
EVSEs), so no fixed threshold avoids both false positives and negatives.
@jHamshaw

jHamshaw commented Jul 19, 2026

Copy link
Copy Markdown

I guess the only issue with this is many people don't have car_charging_now set up, I wonder if we could do it based on load?

Mine is set up like this (Zappi integrated with IOG, 2 x EV's) :

  car_charging_now:
  - sensor.myenergi_zappi_xxxxxxxx_status
  - sensor.myenergi_zappi_xxxxxxxx_status

  car_charging_now_response:
  - "Charging"
  - "Boosting"
  - "SmartBoost"

…sensor

The startup warning for octopus_intelligent_confirm_slots being on without
car_charging_now configured was only ever a self.log() line - easy to miss,
same problem just diagnosed and fixed generally in springfall2008#4273 (an out-of-range
config value silently producing a bad plan because nothing surfaced it
outside the log file). Add a record_status(had_errors=True) call alongside
the existing detailed log message, using the same status-sensor mechanism,
so this shows up as a visible error on the Predbat dashboard.

🤖 Implemented with Claude Code, disclosed per repo convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants