fix(octopus): don't commit the house battery to an unconfirmed Intell…#4270
fix(octopus): don't commit the house battery to an unconfirmed Intell…#4270chalfontchubby wants to merge 4 commits into
Conversation
…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.
|
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? |
|
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.
Mine is set up like this (Zappi integrated with IOG, 2 x EV's) : |
…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>
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_slotspreviously wrote the low rate intorate_importfor 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 viacar_charging_nowconfirming 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_max6-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_nowis 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 withoutcar_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, andcar_charging_nowisn'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_nowat all? Predbat'sdynamic_loadmechanism has a car-slot cancellation check that looks like a candidate, but it isn't one on closer inspection:load_car_slotisn't derived from load telemetry, it's just "are we inside acar_charging_slotswindow" - andcar_charging_slotsis 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 behindmetric_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/car-charging.md) updated to state plainly thatcar_charging_nowis effectively required for this protection to do anything on daytime slots, regardless ofcar_energy_reported_load.car_charging_nowexample (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 mainapps.yamltemplate and all 13 inverter templates carrying the same block.test_rate_add_io_slots.py) and the startup warning (test_fetch_config_options.py).Verification
debug_casescase is unrelated, see prior PR discussion).run_pre_commitpasses cleanly.octopus_slot_maxandoctopus_intelligent_confirm_slotscompose as independent AND conditions with no interference.