Skip to content

fix(loadpoint): surplus counts our orders, not a charger's late reading - #1850

Merged
frahlg merged 5 commits into
masterfrom
fix/surplus-late-readback
Oct 25, 2026
Merged

frahlg merged 5 commits into
masterfrom
fix/surplus-late-readback

Conversation

@frahlg

@frahlg frahlg commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Cause

Surplus charging measures the solar left for the car as −grid + battery + the car's draw. The car's draw came from the charger's own reading, which Easee Cloud reports about 10 s late. Two things went wrong:

  • After a pause. The charger had stopped, but its reading still showed the old draw. Surplus counted that draw as spare solar. On restart Core stepped the car up every 2 s (in the trace 4,830 → 6,900 → 8,280 W) while nothing had reached the charger yet.
  • After a raise. Core raised again before the first raise had landed. Both raises landed together and went past the surplus.

In both cases the car landed in import and paused, about every 80 s.

Change

go/internal/loadpoint/controller.go: surplus now counts each charger at what the meter may show of our own orders, not at its late reading (surplusOrderAdjustW).

  • A raise on its way claims its room. It counts at its order, and no further raise goes out until a reading shows it. "Shows" means a reading under the order, then one reaching it, both after it was sent; at most 180 s. Meanwhile only import on the meter lowers it.
  • A charger ordered off counts at 0.
  • A smaller cut keeps the reading, which shows the draw the meter still has until the cut lands.

A charger whose readings keep up with its draw ramps a little slower: each raise waits until a reading shows it. On the matrix's fast-charger surplus rows that costs about 0.2 kWh in an hour, 4.5 → 4.3 and 3.8 → 3.6 kWh. That is solar not used, not import.

Deleted: nothing in production code.

Numbers

On master 2ec5a1d, which has #1847, #1849 and #1852, the field matrix goes from 19 failing cases, all readback, to 0 of 342. The known file now lists no rows, and its labels are gone.

sitesim "9 kW surplus, readback 10 s late" (5 min), mean power to the car:

master 3606fd1 this branch
phase currents as today 547 W, fails "idle with room" 8,577 W
phase currents signed (#1782's change, applied locally) 359 W, fails 8,793 W
for reference, the same row with no readback lag 8,600 W / 8,821 W unchanged

Field matrix: 342 cases. On master 30 fail (idle 23, own-raise 7); here 23 fail (idle 16, own-raise 7).

  • Deleted from testdata/sitesim_field_known.txt, because these rows pass now:
    • timing/25A/{3p/3p,auto/mixed}/{easee40s,cloud60s,easee120s}/car3p/L1/surplus idle readback (6 rows);
    • probe/25A/auto/mixed/easee40s/car3p/balanced/surplus+goal+export2500W idle readback.
  • Deleted the sitesim scenario's known line for "9 kW surplus, readback 10 s late".
  • Relabelled timing/25A/empty/none/{easee40s,cloud60s,easee120s}/car3p/L1/surplus from readback to nostep. With phase_mode unset and no step table these never order anything, even with a fast charger, so readback was never their cause. The header now names nostep and drops readback.

With signed phase currents applied locally as well, 15 cases fail. Both 25 A readback groups pass, and so do all eight 16 A export rows, including the six that the direction PR finds hunting like these. What still fails: 8 idle rows in empty/none, which are the nostep cause, and the 7 own-raise relief rows.

Tests:

  • New TestSurplusCountsOurOrdersNotALateReading (loadpoint).
  • The sitesim readback row, which failed on master and now passes.
  • TestSiteSimRandomHouse and TestSiteSimScenarios are green, with no new fuse or own-raise failures.
  • go vet and go test -p 2 ./internal/loadpoint/ ./internal/control/ ./cmd/ftw/ are green.

Independent review (Sonnet)

No way to import more than master was found. 316 of 316 non-surplus matrix rows are unchanged; only 9 surplus rows differ.

  • Fixed in 04a61e8. A raise showed only after a reading under 0.9 × the order. A one-step raise from 9 A up (6,210 → 6,900 W) therefore never showed, and held the next raise for the full 180 s even on a fast charger. A reading now shows a raise once it passes halfway from the order before, after one short of halfway, so a high reading from before a cut still does not count. The new cases are in TestSurplusCountsOurOrdersNotALateReading.
  • Open, not a regression: a late reading after a cut lands is still read as truth for about 10 s. The cut path is unchanged here. The re-raise after it is bounded, but not removed.
  • Open, conservative: a charger Core cannot observe keeps a frozen reading, and the "ordered off" subtraction has no time cap. That means less charging for a second surplus charger, never import.
  • Open: SurplusReserveW still uses the late reading, so the home battery can take room a raise on its way has claimed. There is no two-charger or battery surplus case in the matrix.
  • The new helper test checks the arithmetic. No test yet calls computeSurplusCmd end to end with each half removed.

A second look at the halfway rule found nothing blocking.

  • A stale high reading from before a cut still does not count. At worst the rule holds a raise too long, never too early.
  • On a fast charger the ramp is now the same as master's: 6,900 → 7,590 → 8,280 → 8,970 W in 10 s steps. An Easee-like charger (5 s delay, 10 s lag) ramps cleanly here and pause-cycles on master.
  • Nits left open: c.surplusOrders takes no lock. It is touched only from the tick goroutine, but the neighbouring maps use surplusMu. Another loadpoint's raise can inflate this one's room, which is part of the existing two-surplus-charger TODO.

Hardware

Not yet run on a real Easee Cloud charger. Real-hardware evidence is still needed: a surplus session through a pause and restart on Easee Cloud.

🤖 Generated with Claude Code

Easee Cloud reports a charger's draw about 10 s late. Surplus added
that reading back as the car's draw, so after a pause it still counted
the stopped car's old draw as spare solar, and after a raise it raised
again before the first had landed. Both landed in import and the car
paused about every 80 s.

Surplus now counts each charger at what the meter may show of our
orders. A raise on its way already claims its room: it counts at its
order and no further raise goes out until a reading shows it, so only
import lowers it meanwhile. A charger ordered off counts at 0. A
smaller cut keeps the reading, which shows the draw the meter still
has until the cut lands.

Seven field-matrix readback rows and the sitesim row "9 kW surplus,
readback 10 s late" pass. The three 25 A empty/none rows never order
anything, also with a fast charger; they are relabelled nostep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 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-25T14:04:29.484095Z 2878b1b 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: 2878b1b716

ℹ️ 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 +1966 to +1967
adjW, raisingW := c.surplusOrderAdjustW(now, lpCfg.ID)
instant := surplusW + adjW

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 Apply the order adjustment before flooring surplus at zero

When solar disappears after a restart but before the charger's readback catches up, this adjustment can hide grid import entirely. The production reader in go/cmd/ftw/main.go calls SurplusAvailableForEVW, which already floors negative surplus to zero (go/internal/loadpoint/surplus_reserve.go:127–135). For example, with a pending 6,900 W order, a late 0 W EV reading, no battery flow and 6,900 W grid import, the reader returns zero and this adjustment turns it back into 6,900 W of apparent surplus. Surplus-only charging therefore continues importing until readback catches up or the settle timeout expires, instead of reducing immediately. Apply the adjustment to the signed surplus before its zero floor so the import remains visible.

Useful? React with 👍 / 👎.

Comment thread go/internal/loadpoint/controller.go Outdated
Comment on lines +2086 to +2089
if reading < 0.9*order {
o.below = true
} else if o.below {
o.shown = 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 Allow small raises to settle without a ten-percent dip

For normal one-amp raises at higher charging currents, neither the old nor the new reading satisfies this condition, so o.below never becomes true and the raise cannot be acknowledged. For example, a three-phase 10 A → 11 A raise changes the order from 6,900 W to 7,590 W, but the previous 6,900 W reading is already above 0.9 * 7590 (6,831 W). Even after telemetry reports exactly 7,590 W, raisingW remains set and the new raise gate prevents using additional available solar for the full three-minute timeout. Track evidence relative to the previous order or fresh telemetry so small, successfully applied raises can settle too.

Useful? React with 👍 / 👎.

frahlg and others added 4 commits October 25, 2026 15:25
Review: a raise counted as shown only after a reading under 0.9 x the
order, so a one-step raise from 9 A up (6210 -> 6900 W) never showed and
held the next raise for the full 180 s, even on a fast charger. A
reading now shows a raise once it passes halfway from the order before,
after one short of halfway; a high reading from before a cut still does
not count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…adback

# Conflicts:
#	go/internal/control/testdata/sitesim_field_known.txt
@frahlg
frahlg merged commit af9c029 into master Oct 25, 2026
21 checks passed
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