Skip to content

fix(ev): plan from a newer car SoC than the manual level - #1550

Merged
frahlg merged 8 commits into
masterfrom
cursor/ev-planning-vehicle-soc-e54e
Oct 11, 2026
Merged

frahlg merged 8 commits into
masterfrom
cursor/ev-planning-vehicle-soc-e54e

Conversation

@frahlg

@frahlg frahlg commented Oct 11, 2026

Copy link
Copy Markdown
Member

Problem and result

Göran Andersson's charger card showed the Audi at 78% from vag_vehicle (34 minutes old) while the plan still started at 27%, the old slider or plug-in assumption, and scheduled about 50 kWh up to 80%. The car then delivered no power and the card said Not following.

The plan now starts from that car reading. The charging view's "Planning from" line uses the same number.

Root cause

Two different freshness rules were in force.

  • Display (PickBestVehicleForDisplay, from fix(ev): preserve vehicle SoC provenance and age #1470) keeps a cached or driver-stale vehicle SoC and shows its age. That is the 78% / "From Car · 34 min old" line.
  • The planner only accepted a live reading: PickBestVehicleForLoadpoint requires VehicleMaxAge (5 minutes) and rejects stale=true. vag_vehicle marks a portal report stale after 22.5 minutes and emits it with soc_fresh=false. A 34-minute report therefore never became the plan's start SoC.
  • The plan then used the session estimate. That was the 27% level, still unconfirmed (soc_source assumed), which is why the card also said "confirm the current level". The slider was hidden whenever any car SoC was on screen, so the stale 27% could not be corrected and was never replaced.

A second hole remained after the anchor-picker change in #1549: a report that had once been fresh, then was replayed with stale=true as it aged past the driver's own fresh window, was still dropped. That is the usual VW/Audi portal path (a file arrives within 20 minutes, then the same measurement is replayed until it is about 45 minutes old).

Freshness policy

  • Live control, charge limit, and goal completion stay on the 5-minute window and still reject a driver stale flag. A stale charge limit does not cap the plan. A cached report cannot mark the goal complete.
  • Planning start and the session anchor use the existing one-hour window (VehicleAnchorMaxAge). Cloud sources such as the VW Group portal report about every 15 minutes; the driver itself stops emitting after 45 minutes. Inside that hour, a cached report (soc_fresh=false) and a driver-stale replay both count. The plan starts from the measurement plus energy the session has delivered since that time.
  • Which level wins: a car measurement newer than the manual confirmation replaces it. An unconfirmed plug-in assumption (the 27% that still asks to be confirmed) loses to any car reading inside the hour. A manual confirmation newer than the measurement wins, and the slider stays available while the car report is not a live reading so that correction can be entered.
  • Beyond one hour, or with no vehicle driver, the manual or session level remains the plan start.
  • planning_soc / planning_from_vehicle on the loadpoint are that same choice, and the charging view plans from them.

Scope and safety

This branch includes the commits from #1549 (cached observations can anchor, slider stays available for a stale displayed report) and continues from there. Merging this covers that PR; the two should not both land.

Not in scope: when the car is already at its own limit and the charger reports no power ("limited by charger current"), the plan can still offer energy until the existing completion latch decides the car has stopped asking. A stale charge limit is intentionally not treated as that stop point. That is a separate refusal-to-charge behaviour, not the 27% vs 78% start.

Positive power remains into the site. The planner output is still validated by core before any command. No driver or optimizer change.

Verification

  • go test ./internal/telemetry/ ./internal/loadpoint/ ./internal/api/ -run 'Planning|Vehicle|Cached|Anchor|LoadpointPlanning|DecorateLoadpoints|PickBest|PickVehicle'
  • Full go test ./internal/telemetry/ ./internal/loadpoint/
  • node --test web/ev-plan.test.mjs web/ev-plan-status.test.mjs
  • go test -c ./cmd/ftw/ (package builds)
  • go vet on telemetry, loadpoint, api, and cmd/ftw

Covered cases: manual 27% set earlier and a 34-minute driver report of 78% later, the plan starts at 78% (and adds energy delivered after the measurement once); a driver report older than one hour stays on 27%; no driver stays on 27%; a manual level set after the measurement stays on 27%.

The charging-view text is asserted in the web test. I did not drive the full UI in a browser against a vehicle.

Checklist

  • The change follows VISION.md and one selected scope.
  • I checked overlapping PRs and coordinated shared files/contracts.
  • Relevant checks cover the changed behaviour and failure paths.
  • A human reviewed changed UI in a browser, or no UI changed.
  • A Changeset is included, or the change is exempt.
  • Every commit has a DCO sign-off.
Open in Web Open in Cursor 

segran2 and others added 8 commits October 11, 2026 00:33
Signed-off-by: segran2 <268038031+segran2@users.noreply.github.com>
Signed-off-by: segran2 <268038031+segran2@users.noreply.github.com>
Signed-off-by: segran2 <268038031+segran2@users.noreply.github.com>
Signed-off-by: segran2 <268038031+segran2@users.noreply.github.com>
Signed-off-by: segran2 <268038031+segran2@users.noreply.github.com>
Signed-off-by: segran2 <268038031+segran2@users.noreply.github.com>
Signed-off-by: segran2 <268038031+segran2@users.noreply.github.com>
A cloud vehicle report older than five minutes was shown on the charger
card and left out of the plan, so an earlier 27% slider or plug-in
assumption kept scheduling a full charge. A reading inside the one-hour
anchor window now starts the plan when it is newer than that confirmation,
including a cached or driver-stale replay, and the charging view uses the
same number.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
@frahlg
frahlg marked this pull request as ready for review October 11, 2026 05:10
@frahlg
frahlg merged commit f146a1e into master Oct 11, 2026
15 checks passed
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 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-10-11T05:13:13.407672Z 0419ca0 Draft marked ready
ℹ️ 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: 0419ca0710

ℹ️ 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".

// measured SoC — cloud drivers such as vag_vehicle mark a reading
// stale once it leaves their own fresh window, which is well
// inside VehicleAnchorMaxAge. Age, not the flag, bounds planning.
if meta.Stale && use == vehiclePickLive {

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 Keep stale anchors from confirming charge completion

When an idle loadpoint has no confirmation newer than a cached or driver-stale vehicle reading, and that reading is within the one-hour anchor window and at or above the target, this condition admits it through PickVehicleForAnchor; the control loop then passes it to AnchorVehicleSoCAt, which sets completionNotified and publishes ChargingSessionComplete. That can produce a false “Car charged” notification from telemetry explicitly deemed unsuitable for live control, while suppressing the later real completion notification. Stale readings may update the planning estimate, but they must not use the completion-emitting anchor path.

AGENTS.md reference: AGENTS.md:L22-L23

Useful? React with 👍 / 👎.

Comment on lines +1263 to +1264
if !lp.socConfirmed || lp.socConfirmedAt.IsZero() {
return true

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 Persist the manual confirmation timestamp

When a manually corrected session is restored after a process restart, savedSession restores socConfirmed but never socConfirmedAt; this branch therefore treats the restored confirmation as timestamp-less and allows any vehicle observation within the anchor window to replace it, even if that cloud observation predates the user's correction. A restart can thus discard the newer durable manual level and plan from an older cached value, so the confirmation time needs to be persisted/restored or restored confirmations need a precedence-safe fallback.

AGENTS.md reference: AGENTS.md:L20-L21

Useful? React with 👍 / 👎.

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.

3 participants