Skip to content

fix(ev): retain charging progress and restore the matching plan - #1240

Merged
frahlg merged 7 commits into
masterfrom
fix/backup-ev-followup-20260913
Sep 14, 2026
Merged

frahlg merged 7 commits into
masterfrom
fix/backup-ev-followup-20260913

Conversation

@frahlg

@frahlg frahlg commented Sep 13, 2026

Copy link
Copy Markdown
Member

A delayed charger counter could freeze the EV battery estimate while power was flowing. Restarting before the counter caught up then lost progress, and a restored level could leave the planner using the old assumed level. Preserve source times, reconcile counter and power energy without overlap, and retain the anchor under the verified hardware/session identity.

Core uses the same energy calculation for battery progress and charge budgets. Missing or older counters do not reset the anchor; an actual session change or newer counter reset still invalidates it. Checkpoints bound unsaved progress to 30 seconds or 30 Wh, with immediate saves on stop, user correction and failed-save retry. At 75 kWh, 30 Wh is 0.036 percentage points after charging loss; counter catch-up restores the full progress after a crash between checkpoints.

Unavailable charger power now causes an explicit zero command without erasing the request or spent pulse energy. Recovery cannot repeat a spent budget. Restored-level replanning accounts for the possible placement of a duty pulse, so normal peak-power execution does not trigger false replans. The UI explains reached goals and unavailable measurements, and distinguishes an estimate from a level reported by the car.

Paired driver PR: srcfl/device-drivers#116. Core pins d44fda113f171b5824ecbe178073731dfcf1c031. Easee keeps the canonical session through pauses, carries observation timestamps, requires fresh session proof after offline/unreadable data, and spreads failed lookups across the hour. The bounded 180-second source cadence does not extend Core's separate driver watchdog.

Validation:

  • Local make verify passed: Go tests, vet, build, container/update checks and Energyplan 0.4.5 worker verification.
  • Local npm test: 605 passed. Driver make check: 4,404 passed, 1,069 skipped.
  • New controller and planner regressions failed before the fixes: a spent 100 Wh pulse resumed at 11 kW with unavailable power, and valid duty delivery triggered a false replan. Both now pass, including recovery and partial-slot execution.
  • Other regressions cover counter catch-up, out-of-order readings, missing first counters, bounded checkpoint writes, exact stopped-state recovery through SQLite reopen, and full progress recovery after counter catch-up.
  • A physical Easee charge ran at about 7.1 kW and stopped at the target estimate. Measured and commanded power reached 0 W and the temporary hold released. The final ARM64 candidate (67439f4) also passed a paused Core restart and a new bounded charge at about 7.0 kW, followed by measured and commanded 0 W. Its final stopped estimate matched the actual state.db EV record exactly; the saved goal remained unchanged. Outage cases were simulated locally.

This is a development candidate, not a beta release. The battery level is an estimate; the latest BMS value has not been independently confirmed. Full backup/storage reliability in #1234 remains unresolved. Human review of the rendered UI remains required before merge.

Closes #1235
Closes #1236
Closes #1239
Closes #1241
Closes #1242
Closes #1243
Closes #1244
Closes #1245

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 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-09-13T15:01:35.557987Z ab4c77d 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.

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

@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: ab4c77d1c4

ℹ️ 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 28 to 30
if !sample.Connected || sample.ConnectionUnknown || sample.PowerUnavailable || math.IsNaN(sample.PowerW) || math.IsInf(sample.PowerW, 0) {
delete(c.energySamples, cfg.ID)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stop duty dispatch when power measurement is unavailable

When a source-aged power reading sets PowerUnavailable, this deletes all delivery history, but tickOne still proceeds to computeCommand; for plans with LoadpointMaxPowerW, the unmeasured-time reservation leaves a positive remainingWh until slot end and lines 2797-2803 therefore command peak power throughout. For example, a 100 Wh budget in a 15-minute slot can become a full 2.75 kWh at 11 kW whenever both power and the delayed counter remain unavailable, overshooting the operator's planned charge instead of stopping or conservatively bounding dispatch.

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

Useful? React with 👍 / 👎.

Comment on lines +52 to +56
watts := a.LoadpointPowerW[lp.ID]
if len(a.LoadpointPowerW) == 0 && len(previous) == 1 {
watts = a.LoadpointW
}
expected += max(0, watts) * float64(elapsed) / 3600000 * efficiency / initial.CapacityWh

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 Compare SoC against the duty-shaped execution curve

For actions carrying LoadpointMaxPowerW, LoadpointPowerW is the slot-average energy budget, while the controller deliberately runs at peak power until that budget is spent. Integrating the average uniformly therefore reports normal execution as divergence: in a one-hour 60 kWh EV action averaging 5.5 kW with an 11 kW peak, after 15 minutes the measured estimate has risen about 4.1 percentage points while this calculation expects only about 2.1, crossing the 2-point threshold and triggering replans every cooldown. The expected range needs to reflect the actual duty dispatch rather than conflating the planned average with measured progress.

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

Useful? React with 👍 / 👎.

Comment thread go/internal/loadpoint/session_state.go Outdated
Comment on lines +190 to +196
// Save each change inferred from power: the cloud counter can remain
// behind through an arbitrary restart. Unchanged estimates do not write.
saveProgress := lp.socConfirmed && lp.sessionID != "" && lp.energy.counterKnown &&
(lp.socRetention != "session" || (lp.energy.source == "power" && lp.deliveredWhSession != lp.lastSavedEnergyWh))
m.mu.Unlock()
if saveProgress {
m.persistSession(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 Debounce inferred-progress persistence

During confirmed charging with a delayed counter, deliveredWhSession changes on every control observation and energy.source remains power, so this condition calls persistSession for every tick. The default two-second control interval consequently performs roughly 43,200 SQLite upserts per charging day against state.db, adding continuous I/O and flash wear on the Raspberry Pi target merely to reduce restart loss by another two seconds; persist on a bounded interval or meaningful energy delta instead.

Useful? React with 👍 / 👎.

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment