Repository navigation
fix(octopus): ignore planned Intelligent dispatches while Smart Control is off - #5341
chalfontchubby wants to merge 21 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
…n slots come from Ohme (#5341 review) With octopus_intelligent_slot pointed at the Ohme integration, the template's default octopus_intelligent_smart_control still finds the Octopus Energy switch, so Ohme's own planned slots (which carry no source) were dropped whenever Smart Control was off. Tell Ohme-on-IOG users to comment it out, and add the item to the devices.md Octopus Intelligent Go block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit e2da6b3b8c8411edf7c8389274ca8a19417f0a65)
… count four car entities (#5341 review) The switch handler sends SUSPEND/UNSUSPEND even when the switch already shows that state: the cached device state can be up to a poll old, so skipping the call could drop a real change made in the Octopus app just before. The build_discovery docstring now lists four car entities, and a leftover smart_control_confirmed line is removed from the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-planned-slots-5339 Conflicts resolved by keeping both sides: - config.py: APPS_SCHEMA lists both octopus_intelligent_smart_control (this PR) and octopus_intelligent_planned_slot (#5443). - fetch.py: keeps octopus_smart_control_off() and main's car_planned_slot_entity(). - octopus.py: imports take OCTOPUS_MANUAL_DISPATCH_SOURCES as well as main's REPEAT_FULL_LOG_SECONDS/RepeatLogGate. automatic_config() wires the Smart Control switch alongside the dispatch sensor, which the Octopus component still owns, while main's other_owner check (#5443) guards only the ready time and charge limit. - docs/apps-yaml.md, docs/car-charging.md: both new items documented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_planned_slot while Smart Control is off (#5339) Since the merge with main, a car's planned dispatches can come from octopus_intelligent_planned_slot (#5443) rather than the Octopus dispatch sensor. Those are the charger's own plan, not the plan Octopus keeps reporting after Smart Control is switched off, so the Smart Control filter no longer applies to them. Before this, a charger whose sensor does not tag its slots as the charger's schedule lost every planned slot while the switch was off. The Ohme component already tags its slots that way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Smart Control is off (#5339) The previous commit kept the planned slots from octopus_intelligent_planned_slot while Smart Control is off, but did not change their source. While the Ohme component follows Octopus's dispatches it leaves its slots untagged, so they were still costed as off-peak Intelligent dispatches. Octopus is not dispatching anything at that point. They are now re-tagged as a charger schedule: still car load, but never off-peak. Manual bump/boost slots keep their own source. Two further fixes: - If the planned sensor cannot be read, `planned` still holds the slot sensor's Octopus plan. That plan is now filtered as usual, rather than being let through because a planned sensor was configured. - The Smart Control state is now read every fetch again, so the log line for its changes stays on time while a planned sensor is set. Tests: - t10c checks the source, the off-peak test and the switch-on case. - t10d covers an unreadable planned sensor. - t7b pins that automatic_config() still wires the switch with the dispatch sensor while another component owns the ready time and charge limit (#5443). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hile Smart Control is off (#5339) This follows up review findings on the previous commit. - While Smart Control is off, completed dispatches that come only from octopus_intelligent_planned_slot are now tagged as a charger schedule, like its planned ones. Before this, they went back to being costed off-peak as soon as they ended. A dispatch Octopus did make is still on the slot sensor's completed list. That list is merged first, so its own copy wins in load_octopus_slots(). - The planned sensor is used only once both of its reads succeed. Before this, a failed read of completed_dispatches left the charger's plan in place but treated it as the slot sensor's. - A manual bump/boost slot whose source is only under meta is kept with the source at the top level. load_octopus_slots() only reads it there, so before this it costed the slot off-peak. - The filter moves into Fetch.smart_control_off_slots(). - t10d now stubs record_status and restores get_state_wrapper by assignment, so nothing is left on the shared fixture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


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). Slots taken fromoctopus_intelligent_planned_slot(#5443) are the charger's own plan, so they are kept as car load, but re-taggedcharger-scheduleso they are costed at the normal rate rather than as off-peak Intelligent dispatches.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, and still does so when another component (Ohme, fix(ohme): take completed Intelligent dispatches from Octopus, planned slots from Ohme #5443) has claimed the ready time and charge limit, since the dispatch sensor and its switch stay with the Octopus component; 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 (19 cases): on/off/unavailable/unknown/odd state, case, unset item, single and list forms, completed kept, bump/BOOST kept, a charger schedule kept, slots fromoctopus_intelligent_planned_slotkept but re-tagged (planned and finished), an unreadable planned sensor, a bump charge tagged only undermeta, 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. Main was merged in on 12 Oct (after fix(ohme): take completed Intelligent dispatches from Octopus, planned slots from Ohme #5443 addedoctopus_intelligent_planned_slot); the follow-up commits keep that sensor's slots while Smart Control is off, costed as the charger's schedule. A newoctopus_smart_control_switchcase pins that the switch is still wired with the dispatch sensor when Ohme owns the ready time and charge limit.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
octopus_intelligent_planned_slotand are kept while Smart Control is off, at the normal rate. A hand-set planned sensor that only mirrors Octopus's plan would be kept too, as car load; and a dispatch already under way when Smart Control goes off is re-costed whole, including the part Octopus billed off-peak, until the slot sensor lists it as completed. If insteadoctopus_intelligent_slotitself points at Ohme (or another charger's own integration) rather than the Octopus Energy integration, comment outoctopus_intelligent_smart_control. Otherwise the template's default regex still finds the Octopus Energy switch, and Ohme's own planned slots are ignored whenever Smart Control is off.docs/car-charging.mdanddocs/devices.mdsay so.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