Make EV charging controls clear, persistent and consistent - #1074
Conversation
…arge Surplus-only is an EV policy, not a site import ban. The car may use PV left after house load in the same slot the home battery buys from the grid. Live surplus and the near-term 3Φ gate follow the same accounting. Add a joined EV site harness (plan → charger Tick → ComputeDispatch → site identity). Isolated planner, charger and dispatch suites never ran on one clock, which is why the combo stayed invisible. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Give leftover PV, house residual and grid identity a home in loadpoint.GridW. DP, ValidatePlan, main.go and the joined site clock all read that contract. Plan→EMS mapping lives in SlotDirectiveFromMPC and SlotDirective.LoadpointDirective — the same adapters main.go uses. The site clock publishes via Service.InstallPlan and reads SlotDirectiveAt, so charger and battery cannot be given two mappings of one slot. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
The surplus-only EV tests still used SoCMinPct / InitialSoCPct. Core stores fractions. SlotDirectiveFromMPC uses SoCTarget and LivePVSurplusSoCCap.
…r import Meter import is not grid-funded battery charge. Soak plus EV can import together while the battery is still taking leftover PV. Live surplus and the 3Φ gate now treat grid minus EV as the battery's grid, the same identity PlannedPVSoakW already used on a soak-only tick. ValidatePlan rejects leftover breach. Python leftover-assert matches the new policy. Ship a new optimizer image with this core. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Field report 2026-09-05 22:00 (#1002): Charge now on an Easee over the cloud driver, and the Manual tab said "Charging at 16 A until the car is full" a tenth of a second later. That line was written at click time and never refreshed; the Easee cloud takes 5–15 s to act, the status table showed 0 W, and the operator removed the charger to charge by hand. Everything needed to say what the charger did with the order was already on the wire in /api/ev/status; nothing read it on this path, and the box kept no record of when the hold was installed. The box now keeps that record. ManualHold carries StartedAt, the manager records when its current order was first given (commanded_since_ms), and GET /api/loadpoints carries `manual`: state (sent, accepted, charging, not_drawing, stalled, limited), started_at_ms, since_ms, requested and commanded watts and amps, and the charger's reported limit and reason. ManualStatusFrom is pure and tested tick by tick. The Manual tab is redrawn on every poll from that account, the plan strip says the same sentence while a manual charge runs, and a refused Start reads as a failure with the server's reason instead of the success sentence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
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.
Stale comment
Left a non-blocking comment; not approved, and no reviewers were assigned. Cursor Bugbot reported one unresolved medium-severity finding, and APPROVAL_POLICY.md still requires CODEOWNERS review plus a visual check of the web changes. The only matching CODEOWNER is the PR author, so nobody was requested.
Sent by Cursor Approval Agent: Pull Request Router and Approver
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f73cd99e7b
ℹ️ 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".
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
There was a problem hiding this comment.
Stale comment
Left a non-blocking comment; not approved, and no reviewers were assigned. Cursor Bugbot was present on this head but finished as skipped, so the required automated-review signal did not complete successfully. APPROVAL_POLICY.md also does not authorize auto-approval here; CODEOWNERS and the web visual check still need a human.
Sent by Cursor Approval Agent: Pull Request Router and Approver
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 e7edd06. Configure here.
There was a problem hiding this comment.
Stale comment
Left a non-blocking comment; not approved, and no reviewers were assigned. Cursor Bugbot was present on this head but finished as skipped, so the required automated-review signal did not complete successfully. APPROVAL_POLICY.md also does not authorize auto-approval here; CODEOWNERS and the web visual check still need a human.
Sent by Cursor Approval Agent: Pull Request Router and Approver
There was a problem hiding this comment.
Left a non-blocking comment; not approved, and no reviewers were assigned. Cursor Bugbot passed on this head with no current findings, but APPROVAL_POLICY.md still does not authorize auto-approval: CODEOWNERS review and a visual check of the web changes remain required. The only matching CODEOWNER is the PR author, so nobody was requested.
Sent by Cursor Approval Agent: Pull Request Router and Approver



An EV owner can lower the requested current while the car still draws 11 kW without knowing whether the charger received the change. Charging now has one view that separates the request, the charger response and measured power. Opening a section does not change charging mode.
This includes the EV allocation work in #957 and restart work in #1073; coordinate those existing PRs before merge. Easee 1.3.0 is pinned from srcfl/device-drivers#108. This beta uses Core DP. Core and updater need builds from the same release commit; a new external optimizer release is not required. The paired Webapp branch includes srcfl/ftw-webapp#55.
Validation: full make verify, focused race tests and all 533 Core web tests pass. Driver verification passes 4037 tests with 1309 skips. Tests cover command acknowledgement, fuse limits, persistence failures, session changes, cancellation, cloud delays, autosave and planning failure. Held-planner tests prove that writes return before planning finishes and that outdated windows stay hidden. Paired Webapp verification passes 1017 tests with 1 skip on Node 24.20.0, with no Svelte errors or warnings. Build and test checks pass on the exact final commits in all three code PRs.
A mobile owner/passkey walkthrough passed on Core 6843ddd and Webapp 8ead63a3 against the real box. The goal slider saved 85% by 07:00 in 1.46 seconds while planning continued, then reached a current plan. Both clients retained the goal after reload; an early car tap during reconnect opened the panel. Remove showed Goal removed and cleared the target and deadline. Both clients saved battery size 60→61→60 kWh without Save while unplugged. The App retained each value after a full reload; Core retained the restored value after reload. No browser errors occurred. The temporary identity was revoked and the original four identities remain.
The local test installed Core, updater and a separate shadow optimizer from this head. The active planner is Core DP; that three-image test setup is not a release dependency. Real UI restart kept the same container and image. A full backup was created, separately verified and restored to an isolated directory; SQLite quick_check and the restored driver hash passed.
Limits: the car is unplugged, so the final walkthrough cannot prove physical current reduction or delivery to a closed phone app. Earlier current tests used an older candidate. The external optimizer runs in shadow mode while Core DP supplies the active plan. The separate optimizer publication probe returned HTTP 403, but beta.yml only publishes Core and updater. That failure does not block this Core DP beta and needs no package-permission change for this release. No release has been published.
Review follow-up: the capacity/restart finding was a false positive. Manager.Load already reanchors and calls persistSession when capacity changes. TestConfirmedBatteryLevelSurvivesDatabaseCloseAndReopen now proves 60→100 kWh, a real SQLite close/reopen, and 84%→84.6% after another 600 Wh. See go/internal/loadpoint/session_sqlite_test.go and b5b0eae. All 12 review findings have code and regression evidence: 11 fixed and one proven false positive. Immutable SDK socket aliases bind old requests, replies and commands to their original connection. Real SDK tests prove that they cannot mutate a replacement connection in either OCPP version. Fresh OCPP recovery has real protocol tests for 1.6 and 2.0.1, adoption, failed triggers, late status and reconnects between controller ticks. These are protocol tests, not a new physical charger test.
Contract-pair: srcfl/ftw-webapp@8ead63a