feat(ev): Start becomes "charge now → target", releasing back to the plan - #1005
feat(ev): Start becomes "charge now → target", releasing back to the plan#1005frahlg wants to merge 1 commit into
Conversation
…plan Field report (#1002): a Start hold pins the wallbox at a fixed amperage until unplug, silently disabling the planner for the whole session — observed as an 11 kW offer held all night against a car that was already full, with the fuse clamp fighting it the whole way. ManualHold gains ReleaseAtSoC (0–1): once the loadpoint's inferred (or BMS-anchored) SoC reaches it, the controller clears the hold and the same tick falls through to automatic surplus/plan dispatch. The target persists with the hold across restarts; a hold without one keeps the legacy pin-until-Stop-or-unplug contract, and the existing releases (unplug, vehicle-declined debounce) still apply. POST /api/loadpoints/{id}/manual_hold accepts release_at_soc_pct (0–100), GET mirrors it, and the loadpoint state carries manual_release_soc so the UI can say where the boost stops. The modal's Start button becomes "Charge now → 80 %" (schedule target when set, 80 % otherwise) and both the manual tab and the plan strip explain the hand-back. Verified in the running dev stack: pressed the button, hold installed with release_at_soc_pct 80; corrected SoC to 85 %; controller logged "charge-now target reached" and released to plan dispatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1d4uknzs7QUv7nE9rW4fi
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d47879cfbb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // surplus/plan dispatch instead of holding the wallbox at a fixed | ||
| // amperage the rest of the session. | ||
| if hold, held := c.GetManualHold(lpCfg.ID, now); held && hold.ReleaseAtSoC > 0 { | ||
| if st, ok := c.manager.State(lpCfg.ID); ok && st.CurrentSoC >= hold.ReleaseAtSoC { |
There was a problem hiding this comment.
Honor charge-now targets when capacity is omitted
When a loadpoint has neither vehicle_capacity_wh nor a paired vehicle BMS, this condition can never become true: configuration permits omitting capacity, while estimateSoC in loadpoint.go returns only the fixed plug-in anchor whenever capacity is zero, regardless of delivered session energy. The UI now installs every Charge-now hold with an 80% or scheduled release target, but on these sites the hold continues past that target until Stop, unplug, or a supported vehicle-declined signal—recreating the persistent high-current behavior this change is meant to prevent. Apply the documented capacity fallback to SoC inference or otherwise ensure target-based holds can terminate without configured capacity.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d47879c. Configure here.
| slog.Info("loadpoint manual hold released — charge-now target reached", | ||
| "lp", lpCfg.ID, "soc", st.CurrentSoC, "release_at_soc", hold.ReleaseAtSoC) | ||
| c.ClearManualHold(lpCfg.ID) | ||
| } |
There was a problem hiding this comment.
Charge now uses latched session SoC
Medium Severity
Charge-now release compares ReleaseAtSoC to CurrentSoC after Observe, which pins CurrentSoC to targetSoC once sessionComplete latches. The UI sends the schedule target as the release point, so a hold clears on the first tick even when the car is still below that target — typically after it declined at its own charge limit. Charge now then cannot override a completed session without an unplug.
Reviewed by Cursor Bugbot for commit d47879c. Configure here.




Part 2 of #1002. Stacked on #1004 — retarget to master after #1004 merges; only the last commit is this PR's diff.
Problem
The modal's Start button installs a persistent manual hold that pins the wallbox at a fixed amperage until unplug — the planner is silently dead for the whole session. On 2026-08-29 that meant an 11 kW offer held all night against a car that was already at its own charge limit, with the per-phase fuse clamp fighting the hold the entire time (worst phase measured 23 A on a 16 A main fuse).
What this does
loadpoint.ManualHoldgainsReleaseAtSoC(0–1): once the loadpoint's inferred (or BMS-anchored) SoC reaches it, the controller clears the hold and the same tick falls through to automatic surplus/plan dispatch. Persisted with the hold across restarts. A hold without a target keeps the legacy pin-until-Stop-or-unplug contract; unplug and the vehicle-declined debounce still release as before.POST /api/loadpoints/{id}/manual_holdacceptsrelease_at_soc_pct(0–100, validated); GET mirrors it; loadpoint state carriesmanual_release_soc.Verified in a browser (running dev stack)
Pressed "Charge now → 80 %" in the rendered modal → hold installed with
release_at_soc_pct: 80→ corrected SoC to 85 % via/soc→ controller loggedloadpoint manual hold released — charge-now target reached (soc=0.85, release_at_soc=0.8)and the very next dispatch was the plan's, not the hold's.Tests
TestChargeNowHoldReleasesAtTargetSoC— holds below target, releases at target, same-tick plan dispatchTestLegacyHoldWithoutTargetNeverSoCReleases— legacy contract untouchedweb/ev-charge-now.test.mjs— button contract, POST body, both status texts (445 web tests green)🤖 Generated with Claude Code
https://claude.ai/code/session_01C1d4uknzs7QUv7nE9rW4fi
Note
Medium Risk
Changes live EV dispatch when operators use Start—release timing depends on inferred SoC—but legacy holds are unchanged and behavior is covered by new tests.
Overview
Start is a bounded “charge now” boost, not a session-long planner override. The EV modal relabels the action to “Charge now → {target} %” (schedule target SoC, else 80 %), posts
release_at_soc_pctwith the manual hold, and updates plan/manual status copy to describe hand-back to the plan.Backend:
ManualHoldgainsReleaseAtSoC; the loadpoint controller clears the hold when estimated/BMS SoC reaches the target and dispatches the MPC plan on the same tick. Holds with no release target keep pin until Stop/unplug.POST/GET /api/loadpoints/{id}/manual_holdaccept and echorelease_at_soc_pct(0–100); loadpoint state exposesmanual_release_soc.Tests: controller release-at-target + legacy hold behavior; web test asserts UI POST/labels.
Reviewed by Cursor Bugbot for commit d47879c. Bugbot is set up for automated code reviews on this repo. Configure here.