Repository navigation
fix(octopus): ignore planned Intelligent dispatches while Smart Control is off - #5341
chalfontchubby wants to merge 15 commits into
Conversation
|
Repeating @PianSom's comment on this PR here. Can I just check - if the new switch is put in apps.yaml and is set to eg on, but I then (programatically through an automation call to the Octopus api) turn off Smart Control then will Predbat pick up this change and ignore the apps.yaml setting? I had (naively?) thought a Config switch may be better than an apps.yaml switch. |
springfall2008
left a comment
There was a problem hiding this comment.
Thanks for this — the fix is right for the Octopus Energy (BottlecapDave) integration, and I checked upstream that the switch is not isSuspended and that planned dispatches are published regardless.
Two changes I'd like before this merges.
Add the real control to the built-in Octopus component. octopus.py publishes no switch, and its dispatch sensor (..._intelligent_dispatch_<n>) doesn't match the derive regex, so this PR does nothing there.
- Publish a switch per device, e.g.
switch.<prefix>_octopus_<account>_intelligent_smart_charge_<n>, with statenot suspended. Publish it for every device, including suspended ones, so it can be turned back on. - Make it writable. Turn on/off should queue a command that sends
updateDeviceSmartControlwithUNSUSPEND/SUSPEND, following the existing target time / target SoC commands. Set the state straight away rather than waiting for the next poll. - Wire it in
automatic_config(): setoctopus_intelligent_smart_controlalongsideoctopus_intelligent_slot, for active devices only. - Keep excluding suspended devices from the wiring. Accounts can carry a lot of stale devices and we don't want them using up
num_cars.
The result: turning the switch off in HA makes Predbat ignore the planned slots on its next run, and the next device poll then unwires the car as it does today. A change made in the Octopus app arrives in the same poll as the unwiring, so that case is unchanged.
Drop the hard-wired derive from the slot sensor name.
-
Rather than deriving the switch by regex in
fetch.py, configure it like every other Octopus Energy entity. Add this to the templates andapps/predbat/config/apps.yaml, next tooctopus_intelligent_slot:octopus_intelligent_smart_control: 're:(switch.octopus_energy([0-9a-z_]+|)_intelligent_smart_charge)'
Existing users won't pick this up automatically, so please update the docs to say so and add a line for the release notes telling Octopus Energy integration users to add it to their
apps.yaml. Some templates have CRLF line endings — check the diff is only the one added line per file.
Please add tests for the switch state, both commands, and the auto-config wiring, and remove the derive cases from the existing test.
|
Posted by Claude on behalf of @chalfontchubby. Pushed Hard-coded regex (review comment on Does Predbat follow the switch if Smart Control is turned off by an automation? Yes. The apps.yaml item holds the entity ID of the Octopus Energy switch, not an on/off value. Predbat reads that switch's state on every fetch, so it follows it however it is turned off: the Octopus app, Home Assistant or an automation. The only lag is the Octopus Energy integration's own refresh of the switch. Should it be configurable at all, or just follow the Octopus switch? It follows the Octopus switch. The apps.yaml item only says which entity that is, and the regex default finds it for most users with nothing to set. A Predbat-side HA control was considered, but it would be one per car and add nothing the Octopus switch does not already say. One consequence to be aware of: users whose apps.yaml predates this item need to add the line (the docs say so). Without it, behaviour is unchanged from today. The PR description is rewritten to match. |
|
Question whether this needs to be configurable with more apps.yaml config options ? If smart charging off in HA then shouldn't predbat ignore the slots as the octopus component does ? Is there ever a valid case for predbat using the dog smart charging slots if smart charging config is off? |
|
The only thing added to apps.yaml is an optional entity id for the octopus switch which we should default to follow correctly - there should be no need to add anything for new users - existing users might need to add the entity id to wire it in, but we don't add our own switch |
ok, so its not an apps.yaml true/false config switch which is what @PianSom and I thought it is, its a regex config option to wire predbat to the octopus integration switch that the BCD integration already exposes |
|
Posted by Claude on behalf of @chalfontchubby. Pushed
The regex default in the templates and One consequence of point 4: turning the switch off unwires that car on the next poll, so later cars can move down one slot. Your description says that already happens for a change made in the Octopus app; an HA click can now cause it too. @gcoan: yes, for Octopus Energy integration users |
|
Automated comment from the triage bot (PR cleanup run). Merged
|
|
Automated cleanup failed to complete for this PR - see the triage bot's logs for details. The run exited cleanly but left 6 file(s) with uncommitted changes, so its fixes were never pushed. They are stashed in the bot's clone before the next flow runs. Not retrying automatically; remove |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Queued Smart Control mutations can roll back to an unconfirmed optimistic state after multiple failures.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Adds Smart Control awareness so Predbat ignores obsolete planned Octopus dispatches when control is explicitly off.
Changes:
- Filters planned slots while preserving completed and manual dispatches.
- Adds a built-in Smart Control switch with Octopus mutation handling.
- Updates configuration, documentation, templates, discovery, and tests.
| File | Description |
|---|---|
apps/predbat/config.py |
Defines the new configuration item. |
apps/predbat/config/apps.yaml |
Adds its default entity regex. |
apps/predbat/const.py |
Shares manual-dispatch source constants. |
apps/predbat/fetch.py |
Filters planned dispatches based on switch state. |
apps/predbat/octopus.py |
Implements the direct Octopus switch and mutation. |
apps/predbat/predbat.py |
Initializes Smart Control logging state. |
apps/predbat/unit_test.py |
Registers the new test suites. |
apps/predbat/tests/test_octopus_intelligent_devices.py |
Extends discovery expectations. |
apps/predbat/tests/test_octopus_smart_control.py |
Tests dispatch filtering behavior. |
apps/predbat/tests/test_octopus_smart_control_switch.py |
Tests the built-in switch. |
coverage/apps.yaml |
Adds the optional test configuration. |
docs/apps-yaml.md |
Documents the configuration item. |
docs/car-charging.md |
Documents behavior and setup. |
templates/alphaess_cloud.yaml |
Adds the default switch mapping. |
templates/enphase_cloud.yaml |
Adds the default switch mapping. |
templates/ep_cube_cloud.yaml |
Adds the default switch mapping. |
templates/fox.yaml |
Adds the default switch mapping. |
templates/fox_cloud.yaml |
Adds the default switch mapping. |
templates/fronius.yaml |
Adds the default switch mapping. |
templates/ginlong_solis.yaml |
Adds the default switch mapping. |
templates/ginlong_solis_fb00.yaml |
Adds the default switch mapping. |
templates/givenergy_cloud.yaml |
Adds the default switch mapping. |
templates/givenergy_ems.yaml |
Adds the default switch mapping. |
templates/givenergy_givtcp.yaml |
Adds the default switch mapping. |
templates/hanchu_cloud.yaml |
Adds the default switch mapping. |
templates/luxpower.yaml |
Adds the default switch mapping. |
templates/sigenergy_cloud.yaml |
Adds the default switch mapping. |
templates/sigenergy_sigenstor.yaml |
Adds the default switch mapping. |
templates/sofar.yaml |
Adds the default switch mapping. |
templates/sofar_modbus.yaml |
Adds the default switch mapping. |
templates/solar_assistant_growatt_spa.yaml |
Adds the default switch mapping. |
templates/solar_assistant_growatt_sph.yaml |
Adds the default switch mapping. |
templates/solaredge.yaml |
Adds the default switch mapping. |
templates/solax_cloud.yaml |
Adds the default switch mapping. |
templates/solax_sx4.yaml |
Adds the default switch mapping. |
templates/solis_cloud.yaml |
Adds the default switch mapping. |
templates/sunsynk.yaml |
Adds the default switch mapping. |
templates/tesla_powerwall.yaml |
Adds the default switch mapping. |
templates/teslemetry.yaml |
Adds the default switch mapping. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ol is off (#5339) Octopus keeps returning the plan it made before Smart Control was switched off (isSuspended true, flexPlannedDispatches unchanged), and the Octopus Energy integration relays it unchanged, so Predbat kept costing the planned and bonus slots at the cheap rate. When the Smart Control switch reads off, skip the planned dispatches of that car and keep the completed ones. The switch is octopus_intelligent_smart_control (optional, one per car), or is derived from the integration's intelligent_dispatching sensor name. Only an explicit "off" counts, so a missing or unavailable switch behaves as before. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Derive the switch for the legacy sensor name without a device id, only log the Smart Control change on a live fetch (not compare.py re-runs), fold the skip into the existing planned-slot condition so the switch is read only when there are planned slots, and cover multi-car, short lists, case and logging in tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s off (#5339) A bump or boost charge is something the user asked for, so Smart Control being off does not void it. Also read the switch on every fetch so a change is logged even when no dispatches are planned, and restore more fetch state in the test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…an in the timeline (#5339) Move the bump-charge/BOOST sources to const.py so rate costing and the Smart Control filter agree, read a source nested under meta, leave the diagnostic timeline showing what Octopus reported, and test BOOST. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d-coded regex (#5339 review) Drop the derivation of the switch from the dispatching sensor's name. The default now lives in the apps.yaml templates as a regex, like octopus_intelligent_slot, octopus_ready_time and octopus_charge_limit, so it can be overridden or removed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…pps.yaml (#5339) Commented out like its neighbours, so test behaviour is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-in component (#5339 review) The built-in Octopus component publishes switch.<prefix>_octopus_<account>_intelligent_smart_charge_<n> for every Intelligent device, suspended ones included, on while the device is not suspended. Turning it on or off shows the new state straight away and queues updateDeviceSmartControl UNSUSPEND / SUSPEND, which puts the switch back if Octopus rejects it. automatic_config() wires it into octopus_intelligent_smart_control for active devices only, so suspended devices stay unwired. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Keep a change we have sent on show for two minutes so a poll straight after it cannot flip the switch back, put the switch back on an exception or an empty reply too, match the device by its exact index suffix, and name the switch after the car. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… being off, not "On" (#5341 review) An unknown or unavailable switch is no evidence either way, so the log no longer claims Smart Control came back on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… confirmed (#5341 review) Each queued command used to carry the state shown just before it, which after an earlier optimistic change was never confirmed by Octopus. Queueing off then on, with both failing, left the switch off although nothing had changed. The state Octopus last confirmed is now kept per device until every change from the switch has been sent, a rollback goes back to that, and only the latest queued change per device is sent. A change made while another is being sent stays on show until it has been sent too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vice poll (#5341 review) A switch change made while a poll was in flight was overwritten by the poll's older state before it had been sent, so the switch flipped back and a toggle then read the wrong state. The poll's state now becomes the one a failed send goes back to, and the unsent change stays on show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orget a gone device's baseline (#5341 review) The 2-minute hold expired before the second 2-minute device poll, so a slow Octopus update flipped the switch back on that poll and flipped it again a poll later. It is now 5 minutes. A change queued for a device that has since gone no longer leaves its confirmed state behind for a later rollback to use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#5341 review) When the Ohme component wires the car slots to the charger's own schedule, the Smart Control switch can still read off. Those slots are the charge the charger will run, not Octopus's plan, so they are no longer dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, not before (#5341 review) The switch used to show a change straight away and roll it back on failure. That needed the last confirmed state kept aside per device, and each review round found another way for it to go wrong: changes queued behind each other, a poll in between, a lost command, a poll reusing cached settings. The switch now changes once Octopus has accepted the change, as the target time and SoC already do, so it never shows a state Octopus does not have and there is nothing to roll back. Only the latest change per device is sent, two toggles before it is sent cancel out, and an accepted change is held against polls for its full 5 minutes, since a poll whose settings query failed reuses the state on show and so agreeing is no sign Octopus has caught up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed switch is really published (#5341 review) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9a380e4 to
fbf1610
Compare


Written by Claude on behalf of @chalfontchubby. Rik has not read this text.
Fixes #5339
Summary
When Octopus Smart Control is switched off, Octopus keeps returning the charging plan it had already made, and the Octopus Energy integration relays it unchanged. Predbat did not know Smart Control was off, so it kept costing the planned and bonus slots at the cheap rate. Now, while Smart Control is off, Predbat ignores that car's planned Octopus dispatches. Dispatches already delivered (
completed) still count, as do manualbump-charge/BOOSTdispatches and a charger's own schedule (the Ohme component'scharger-scheduleslots, which Smart Control has no say in).How it works
octopus_intelligent_smart_control(one entity per car,sensor|sensor_list) holds the entity ID of the Octopus Energy integration's Smart Control switch. Predbat reads that switch's live state every fetch, so it follows the switch however it is turned off (Octopus app, Home Assistant, an automation).re:(switch.octopus_energy([0-9a-z_]+|)_intelligent_smart_charge), alongsideoctopus_intelligent_slot/octopus_ready_time/octopus_charge_limit. There is no hard-coded entity naming in the code. Users whose apps.yaml predates the item need to add the line (documented).Fetch.octopus_smart_control_off()returns true only for an explicitoff. An unset, missing, unavailable or unknown switch behaves exactly as before.compare.pyre-runs).switch.<prefix>_octopus_<account>_intelligent_smart_charge_<n>for every Intelligent device (suspended ones too, so Smart Control can be turned back on), on while the device is not suspended. Turning it on or off queuesupdateDeviceSmartControlwithUNSUSPEND/SUSPEND, sent on the component's next cycle like the target time / target SoC commands, and the switch shows the change once Octopus has accepted it. If Octopus rejects it (error, exception or empty reply) the switch stays as it was, so it never shows a state Octopus does not have. Only the latest change per device is sent, two toggles before it is sent cancel out, and an accepted change is held against polls for five minutes, so a lagging Octopus cannot flip the switch back.automatic_config()setsoctopus_intelligent_smart_controlnext tooctopus_intelligent_slot, for active devices only; suspended devices stay unwired, so stale devices do not use upnum_cars.OCTOPUS_MANUAL_DISPATCH_SOURCESmoves toconst.py, shared by the existing "ignore bump-charge for rate costing" check inoctopus.pyand this filter.coverage/apps.yamllists the item commented out like its neighbours, so test behaviour is unchanged.Evidence
Against a live Octopus Intelligent account, switching Smart Control off gives
isSuspended: true/currentState: SMART_CONTROL_OFFwhileflexPlannedDispatchesreturns the same twoSMARTdispatches as before. In the integration, the switch state isnot isSuspended(intelligent/smart_charge.py), andintelligent/dispatching.pypublishesplanned_dispatcheswith no reference to suspended.Testing
octopus_smart_control_switchtest for the built-in switch: state for active and suspended devices, turn on / off / toggle and the mutation sent, the switch unchanged until Octopus accepts and after a failed, raising or empty reply, queued changes collapsing to the latest, a change made while a batch is being sent, the five-minute hold, exact device matching, andautomatic_config()wiring.octopus_smart_controltest (15 cases): on/off/unavailable/unknown/odd state, case, unset item, single and list forms, completed kept, bump/BOOST kept, a charger schedule kept, multi-car, short switch list, slot signature changes, log-once (and an unavailable switch logged as such, not as "On"). It fails without thefetch.pychange and passes with it../run_pre_commitand the quick suite pass./code-review highpasses; the findings addressed are in the follow-up commits. Rebased onto main on 5 Oct, and every commit passes pre-commit and the full suite on its own.Release note
Octopus Energy integration users: add
octopus_intelligent_smart_control: 're:(switch.octopus_energy([0-9a-z_]+|)_intelligent_smart_charge)'to yourapps.yaml(next tooctopus_intelligent_slot) so Predbat ignores planned Intelligent slots while Smart Control is off. Without it, behaviour is unchanged. Users of the built-in Octopus connection need to do nothing.Known limits
fetch_sensor_data_carsreports HIGH only because it sits in the main update loop; it has a single caller and the change is gated on an explicitoff.🤖 Generated with Claude Code