feat(ev): say why the charger is (not) charging and when it will - #1004
Conversation
Field report (#1002): a car plugged in against a schedule sits at 0 W until the cheap slots arrive, the modal looks dead, and the operator presses Start — which overrides the plan for the whole session. The grid-plan deferral (deadline past published prices) is worse: it forces surplus-only semantics with only an INFO log to show for it, so it reads as a PV-only mode nobody chose. GET /api/loadpoints now carries the answer: the next planned charge window (mpc.LoadpointPlanWindows, same MaxPlanAge cutoff as dispatch so a stale plan promises nothing), the grid-deferral flag, and what the controller last commanded (commanded_w/commanded_known, so "offering power the car won't take" is distinguishable from "pausing on purpose"). The EV modal renders one sentence from those fields, refreshed every poll: planned window with clock times and kWh, waiting- for-prices, car-declined with the charger's own reason, paused-by-box, PV-surplus-only, or the do-nothing default with the three ways out. Closes nothing on its own — #1002 also wants Start to become a bounded "charge now" and the Easee driver to emit request_active (srcfl/device-drivers#103); those land separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1d4uknzs7QUv7nE9rW4fi
Match plan-brief.js's formatClock — the plan UI speaks 24 h regardless of browser locale; toLocaleTimeString gave 01:00 PM on en-US browsers. 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: 137b1eeca2
ℹ️ 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".
| if s.deps.LoadpointCtrl != nil { | ||
| states[i].GridDeferred = s.deps.LoadpointCtrl.GridDeferred(states[i].ID) |
There was a problem hiding this comment.
Clear deferred state when a schedule disappears
When an operator clears a schedule that previously set GridDeferred, the MPC loadpoint-spec loop skips that loadpoint at its schedule gate before calling SetGridDeferred, so the controller's map retains true. Exporting that stale value here makes the modal continue saying it is waiting for tomorrow's prices indefinitely instead of reaching the no-schedule state; clear deferral for loadpoints with no active target or derive this field from current planning inputs.
Useful? React with 👍 / 👎.
| } else if (lp.grid_deferred) { | ||
| text = "Waiting for tomorrow's electricity prices — until they arrive (~13:00) the car charges from PV surplus only."; |
There was a problem hiding this comment.
Don't claim PV-only enforcement for deferred schedules
For a deferred loadpoint with an active schedule, Controller.surplusActive returns false at the sched.SoC > 0 check before consulting gridDeferred. The plan is constrained using forecast surplus, but if live PV falls below that forecast the controller can therefore fulfill the cached EV budget with grid import, making this unconditional “PV surplus only” statement incorrect in exactly that forecast-miss scenario. Either enforce the deferral at runtime or describe it as a planning constraint rather than a live guarantee.
Useful? React with 👍 / 👎.
| } else if (lp.commanded_known && lp.commanded_w > 0) { | ||
| text = "Charger offers " + formatW(lp.commanded_w) + | ||
| " but the car isn't drawing — it may be full or at its own charge limit."; |
There was a problem hiding this comment.
Don't treat a failed dispatch as a charger offer
When ev_set_current fails, the controller has already recorded the requested watts via SetCommandedW, and that value is not cleared on the error path; similarly, a driver later held out by driverCanDispatch retains its previous command. If telemetry then shows the plugged-in car drawing zero, this branch says the charger is offering power and blames the vehicle even though the charger may never have accepted the command. Gate this diagnosis on a successful/current actuation outcome rather than merely the last attempted setpoint.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default 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 137b1ee. Configure here.
| } else if (lp.surplus_only) { | ||
| text = "PV surplus only — charges when solar exceeds house load."; | ||
| } else if (!hasSchedule) { | ||
| text = "Nothing will start charging: set a schedule, turn on PV only, or press Start."; |
There was a problem hiding this comment.
Schedule check ignores unlock intent
Low Severity
hasSchedule treats a loadpoint as unscheduled unless schedule.soc is set, so a surplus-unlock (or recurring) schedule with no SoC target falls through to the “Nothing will start charging” warning. That warning is the do-nothing prompt that pushes operators to press Start, even though surplus unlock can still begin a charge.
Reviewed by Cursor Bugbot for commit 137b1ee. Configure here.




Part 1 of #1002.
Problem
Plug in the car against a schedule and the box correctly waits for the cheap slots — but no surface says so. The modal looks dead, the operator presses Start, and the manual hold silently overrides the planner for the whole session. The grid-plan deferral (
deferGridPlan: deadline past the published price horizon) is worse: it forces surplus-only semantics with only an INFO log to show for it, so any plug-in before ~13:00 with a next-morning deadline reads as a PV-only mode nobody chose.What this does
GET /api/loadpointsnow carries the answer, and the EV modal renders it as one sentence between the status table and the tabs, refreshed on every poll:plan_next_start_ms/plan_next_end_ms/plan_next_wh/plan_total_wh— the next window in which the active plan allocates charge energy to the loadpoint, via a newmpc.LoadpointPlanWindows(handles both the multi-LP and legacy single-LP plan shapes; sameMaxPlanAgecutoff as dispatch, so a stale plan promises nothing). UI: "Charging planned 13:00–14:00. ~33.0 kWh planned. The planner picks the cheapest hours before your target."grid_deferred— the controller's per-LP deferral flag, exported. UI: "Waiting for tomorrow's electricity prices — until they arrive (~13:00) the car charges from PV surplus only."commanded_w/commanded_known— what the controller last ordered, after every clamp, so "offering power the car won't take" is distinguishable from "pausing on purpose". UI: "Charger offers X kW but the car isn't drawing — it may be full or at its own charge limit", plus the charger's ownreason_no_currentlabel when the driver reports one.Verified in a browser
Ran the dev stack with a local plugged-in EV simulator driver, a
carportloadpoint and the real SE4 price feed, and inspected the rendered modal in Chrome:grid_deferred: trueand the waiting-for-prices strip — the exact state that has been reading as "PV only that I never chose".The remaining #1002 items land separately: Start → bounded "charge now to target" (next PR), and the Easee driver emitting
request_active(srcfl/device-drivers#103).Tests
mpc:TestLoadpointPlanWindows*— window merging, cap-with-total, legacy single-LP shape, stale-plan cutoffweb:ev-plan-status.test.mjs— every visibility state + modal wiring (444 web tests green)make verifyclean (pre-commit + pre-pushverify-allincl. cross-compile)🤖 Generated with Claude Code
https://claude.ai/code/session_01C1d4uknzs7QUv7nE9rW4fi
Note
Low Risk
Read-only API and UI surfacing of existing planner/controller state; no change to dispatch or charging logic beyond exposing
grid_deferredand commanded power.Overview
GET
/api/loadpointsnow answers “when will it charge, and why not now?” with new fields: next planned window (plan_next_*,plan_total_wh) frommpc.LoadpointPlanWindows,grid_deferredwhen grid planning waits on tomorrow’s prices, andcommanded_w/commanded_knownso the UI can tell intentional pauses from “power offered but not taken.”The EV modal adds a plan-status strip (
renderEvPlanStatus) under the live status table, refreshed each poll—planned windows, manual hold, grid deferral, car-not-drawing (plus chargerreason_no_current_label), PV-only, and a warning when nothing will start a charge.Backend wiring:
decorateLoadpointsWithPlan, publicController.GridDeferred, and MPC window merging with the sameMaxPlanAgecutoff as dispatch. Tests cover MPC windows and modal copy/wiring.Reviewed by Cursor Bugbot for commit 137b1ee. Bugbot is set up for automated code reviews on this repo. Configure here.