Skip to content

fix(octopus): ignore planned Intelligent dispatches while Smart Control is off - #5341

Open
chalfontchubby wants to merge 15 commits into
mainfrom
fix/smart-control-off-planned-slots-5339
Open

chalfontchubby wants to merge 15 commits into
mainfrom
fix/smart-control-off-planned-slots-5339

Conversation

@chalfontchubby

@chalfontchubby chalfontchubby commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

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 manual bump-charge / BOOST dispatches and a charger's own schedule (the Ohme component's charger-schedule slots, which Smart Control has no say in).

How it works

  • New apps.yaml item 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).
  • Its default is a regex in the apps.yaml templates, re:(switch.octopus_energy([0-9a-z_]+|)_intelligent_smart_charge), alongside octopus_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 explicit off. An unset, missing, unavailable or unknown switch behaves exactly as before.
  • Dropping the planned slots changes the Octopus slots signature, so the existing replan check recomputes the plan. The change of state is logged once, on a live fetch only (not in compare.py re-runs).
  • Built-in Octopus component: it now publishes 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 queues updateDeviceSmartControl with UNSUSPEND / 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() sets octopus_intelligent_smart_control next to octopus_intelligent_slot, for active devices only; suspended devices stay unwired, so stale devices do not use up num_cars.
  • OCTOPUS_MANUAL_DISPATCH_SOURCES moves to const.py, shared by the existing "ignore bump-charge for rate costing" check in octopus.py and this filter.
  • coverage/apps.yaml lists 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_OFF while flexPlannedDispatches returns the same two SMART dispatches as before. In the integration, the switch state is not isSuspended (intelligent/smart_charge.py), and intelligent/dispatching.py publishes planned_dispatches with no reference to suspended.

Testing

  • New octopus_smart_control_switch test 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, and automatic_config() wiring.
  • New octopus_smart_control test (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 the fetch.py change and passes with it.
  • ./run_pre_commit and the quick suite pass.
  • Reviewed by cold subagents and /code-review high passes; 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.
  • The two Copilot findings of 4 Oct: the log now says what the switch reports rather than "On" when it stops being off; and the rollback race is gone, because the switch no longer shows a change before Octopus accepts it.

Release note

Octopus Energy integration users: add octopus_intelligent_smart_control: 're:(switch.octopus_energy([0-9a-z_]+|)_intelligent_smart_charge)' to your apps.yaml (next to octopus_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

  • Built-in component: turning the switch off unwires that car on the next device poll (suspended devices stay unwired, as before), so its planned slots are no longer read at all, and later cars can move down one car slot. That is the existing behaviour for a change made in the Octopus app; an HA click can now trigger it too. A switch for a device that has since left the account keeps its last state and ignores clicks.
  • Kraken / Ohme wire their own sensors with no Smart Control switch, so they are not covered. They can point the item at a suitable switch if they have one.
  • Whether Octopus honours a bump/boost while Smart Control is off is unverified; they are kept because the user asked for them and the rate code already treats them specially.
  • The diagnostic dispatch timeline (IOG future planned dispatches treated as cheap home rate before EV charging earns the slot #4516) still shows what Octopus reported, including the ignored plan.
  • With the Octopus Energy integration, the car keeps its dispatch sensor, so while Smart Control is off it has no future slots and no car charge is planned; the built-in component unwires the car instead, and Predbat plans it as a normal car. Whether that difference should be closed is an open question.
  • Unverified: if the Octopus Energy integration's rate feed keeps pricing the ignored planned slots cheap while Smart Control is off, this PR does not strip that discount.
  • Impact analysis on fetch_sensor_data_cars reports HIGH only because it sits in the main update loop; it has a single caller and the change is gated on an explicit off.

🤖 Generated with Claude Code

@chalfontchubby

chalfontchubby commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

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.

Comment thread apps/predbat/fetch.py Outdated

@springfall2008 springfall2008 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. Publish a switch per device, e.g. switch.<prefix>_octopus_<account>_intelligent_smart_charge_<n>, with state not suspended. Publish it for every device, including suspended ones, so it can be turned back on.
  2. Make it writable. Turn on/off should queue a command that sends updateDeviceSmartControl with UNSUSPEND / SUSPEND, following the existing target time / target SoC commands. Set the state straight away rather than waiting for the next poll.
  3. Wire it in automatic_config(): set octopus_intelligent_smart_control alongside octopus_intelligent_slot, for active devices only.
  4. 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.

  1. Rather than deriving the switch by regex in fetch.py, configure it like every other Octopus Energy entity. Add this to the templates and apps/predbat/config/apps.yaml, next to octopus_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.

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

Posted by Claude on behalf of @chalfontchubby.

Pushed 5edfa257 and 8cd196c8, which answer the three points raised here.

Hard-coded regex (review comment on fetch.py): agreed, and removed. Predbat no longer derives the switch from the sensor's name. octopus_intelligent_smart_control now ships as a regex default in the apps.yaml templates, like octopus_intelligent_slot, octopus_ready_time and octopus_charge_limit, so it can be overridden or removed. Docs and the multi-car example are updated. coverage/apps.yaml lists it commented out like its neighbours, so test behaviour is unchanged.

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.

@gcoan

gcoan commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

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?

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

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

@gcoan

gcoan commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

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

@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

Posted by Claude on behalf of @chalfontchubby.

Pushed f8e9515d and 9bb73091, which cover the first four points of the review.

  1. Switch: 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.
  2. Writable: turn on / off / toggle shows the new state straight away and queues updateDeviceSmartControl with UNSUSPEND / SUSPEND, as the target time and target SoC commands do. A failed command (error, exception or empty reply) puts the switch back, and a change we have sent stays on show for two minutes so a poll straight after it cannot flip it back.
  3. Wiring: automatic_config() sets octopus_intelligent_smart_control next to octopus_intelligent_slot, for active devices only.
  4. Suspended devices stay unwired.

The regex default in the templates and apps/predbat/config/apps.yaml (point 5) went in earlier in 5edfa257. Each template diff is exactly the one added line, and none of the touched files has CRLF endings in main. The derive cases are gone from the existing test, and the new octopus_smart_control_switch test covers the switch state, both commands, rollback and the wiring. The docs and the PR description (which now carries the release note) are updated.

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 octopus_intelligent_smart_control is only an entity ID that wires Predbat to the switch BCD already exposes, not a true/false setting. The built-in component is the one case where Predbat publishes a switch itself.

@chalfontchubby
chalfontchubby marked this pull request as ready for review October 2, 2026 08:11
@springfall2008 springfall2008 added the BOT_CLEANUP Trigger: bot should address PR review feedback and CI failures, then commit and push label Oct 4, 2026
@springfall2008

Copy link
Copy Markdown
Owner

Automated comment from the triage bot (PR cleanup run).

Merged origin/main into this branch (9a380e47) to clear a conflict with the #5316 changes.

  • Conflict: one, in the import block of apps/predbat/octopus.py. This branch added OCTOPUS_MANUAL_DISPATCH_SOURCES to the const import; main added round_out_to_period to the utils import. Both are used in the merged file (octopus.py:3368, and octopus.py:3814 / 3921), so the resolution keeps both. No logic was changed.
  • Review feedback: nothing left to act on. The hard-coded regex thread on fetch.py and the five points in the review of 2 Oct were already covered by 5edfa257, 8cd196c8, f8e9515d and 9bb73091.
  • Checks on the merged tree: the quick suite passes (All tests passed, 4 slow tests skipped). Pre-commit passes every hook except markdownlint-fix, whose findings are all in local untracked-by-this-PR tooling files in the bot's clone (.claude/skills/gitnexus/*/SKILL.md, AGENTS.md, CLAUDE.md), none in files this PR touches. CI was green before the merge and will re-run on this push.

@springfall2008

Copy link
Copy Markdown
Owner

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 BOT_FAILED and re-add BOT_CLEANUP to try again.

@springfall2008 springfall2008 added BOT_FAILED An automated triage/PR attempt failed; see the comment for details and removed BOT_CLEANUP Trigger: bot should address PR review feedback and CI failures, then commit and push labels Oct 4, 2026
@springfall2008
springfall2008 requested a balanced review from Copilot October 4, 2026 18:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Medium severity · 1 Low severity

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.

Comment thread apps/predbat/octopus.py Outdated
Comment thread apps/predbat/fetch.py Outdated
chalfontchubby and others added 9 commits October 5, 2026 21:27
…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>
chalfontchubby and others added 6 commits October 5, 2026 21:30
… 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>
@chalfontchubby
chalfontchubby force-pushed the fix/smart-control-off-planned-slots-5339 branch from 9a380e4 to fbf1610 Compare October 6, 2026 01:50
@chalfontchubby chalfontchubby removed the BOT_FAILED An automated triage/PR attempt failed; see the comment for details label Oct 7, 2026

This branch has not been deployed

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

Labels

bug Something isn't working Octopus

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IOG - Octoplus plan is still taken into account in Predbat plan after Smart Control is switched off

4 participants