Skip to content

feat(ev): say why the charger is (not) charging and when it will - #1004

Merged
frahlg merged 2 commits into
masterfrom
1002-ev-visibility
Aug 30, 2026
Merged

feat(ev): say why the charger is (not) charging and when it will#1004
frahlg merged 2 commits into
masterfrom
1002-ev-visibility

Conversation

@frahlg

@frahlg frahlg commented Aug 30, 2026

Copy link
Copy Markdown
Member

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/loadpoints now 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 new mpc.LoadpointPlanWindows (handles both the multi-LP and legacy single-LP plan shapes; same MaxPlanAge cutoff 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 own reason_no_current label when the driver reports one.
  • The do-nothing default is named: "Nothing will start charging: set a schedule, turn on PV only, or press Start."

Verified in a browser

Ran the dev stack with a local plugged-in EV simulator driver, a carport loadpoint and the real SE4 price feed, and inspected the rendered modal in Chrome:

  • schedule 80 % by 22:00 tonight → planner allocated 33 kWh; strip shows the 13:00–14:00 window with kWh and 24-h clocks;
  • schedule 80 % by 07:00 tomorrow (past published prices) → grid_deferred: true and 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 cutoff
  • web: ev-plan-status.test.mjs — every visibility state + modal wiring (444 web tests green)
  • make verify clean (pre-commit + pre-push verify-all incl. 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_deferred and commanded power.

Overview
GET /api/loadpoints now answers “when will it charge, and why not now?” with new fields: next planned window (plan_next_*, plan_total_wh) from mpc.LoadpointPlanWindows, grid_deferred when grid planning waits on tomorrow’s prices, and commanded_w / commanded_known so 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 charger reason_no_current_label), PV-only, and a warning when nothing will start a charge.

Backend wiring: decorateLoadpointsWithPlan, public Controller.GridDeferred, and MPC window merging with the same MaxPlanAge cutoff 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.

frahlg and others added 2 commits August 30, 2026 09:35
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
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T07:46:27.485272Z 137b1ee PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +29 to +30
if s.deps.LoadpointCtrl != nil {
states[i].GridDeferred = s.deps.LoadpointCtrl.GridDeferred(states[i].ID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment thread web/app.js
Comment on lines +2629 to +2630
} else if (lp.grid_deferred) {
text = "Waiting for tomorrow's electricity prices — until they arrive (~13:00) the car charges from PV surplus only.";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment thread web/app.js
Comment on lines +2622 to +2624
} 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.";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread web/app.js
} 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.";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 137b1ee. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not approved: Cursor Bugbot skipped and left one unresolved finding that needs a human look. Assigned erikarenhill and HuggeK.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant