Repository navigation
test(control): field matrix for EV charging and fuse protection - #1800
Merged
Merged
Conversation
v0.155.0-beta.1 passed our tests and failed on field sites the same night. We tested our defaults: fixed 3p, no step table, planned charging stubbed, and chargers that applied an order at once or exactly when they declared. Easee Cloud applies in 30-60 s and declares 180 s. TestSiteSimFieldMatrix runs Core's EV charging and fuse protection in the closed loop against 314 cases: the field reports (the help report of 22 Oct, the site in #1763, the home box) with every charger timing, a sweep over fuse, phase mode, step table, car phases and house load, and a timing sweep over every request. Planned charging now runs through the real controller: a minimal planner sends the slot's Wh and on-power, from the steps main.go gives the planner. Each case runs in a synctest bubble, so command evidence, the API's verdict and live Home use run on the plant's clock. Invariants, every second: the fuse holds; FTW's limit as before; no fuse cut while every phase is under the limit; no second cut while an unapplied cut covers the overage; a planned charge delivers 90 % of what the fuse allows; planned charging and Charge now never put a three-phase charger on one phase; a charger drawing against its order reads not following; Home use stays measured while every device reads. testdata/sitesim_field_known.txt lists the rows that fail on master and the fix that turns each green. The sim gains the Easee driver's auto split and phase hold, and outside steps no longer count as our raise. The matrix skips -race builds: it finds nothing there and runs 15 times slower. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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. |
Twenty-four rows from #1799's review and fix D: a charger whose table holds only one-phase levels (max 5,520 W, auto), phase mode unset with a goal and a 2,300 W plan slot, surplus-only with a goal and 2.5 kW of export, a held 1p order raised to 3p beside a phase at 20 A, the help report's site with a one-phase car, and a long L1 step under a three-phase car. Surplus cases get the planner's PV forecast as pickSurplusSteps reads it, so the one-phase rule is judged as Core makes it: one phase only when the next 30 min stay below three phases at 6 A. Surplus cuts follow the export and are no longer judged as fuse cuts. The trace also logs a change of reason. New label hold3p: the app sends Charge now as 3p for an auto loadpoint, and a charger whose table stops below 4,140 W gets 0 W. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 22, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
v0.155.0-beta.1 passed review, the 17-case device rig and the Go closed-loop sim, and field sites hit bugs the same night. The tests ran our defaults: fixed 3p, no step table, planned charging stubbed in sitesim, and chargers that applied an order at once or exactly when they declared. Easee Cloud applies in 30-60 s and declares 180 s.
This PR adds a gate for v0.155.0-beta.2: the fast Go sim runs Core against the settings sites run. Test code only; no control logic changes.
What runs
TestSiteSimFieldMatrix(go/internal/control/sitesim_field*_test.go), 338 cases, each in asynctestbubble so the telemetry store, command evidence, the API's verdict and live Home use run on the plant's clock.main.gogives the planner:LimitPower(st, PlanningSteps(...)). It replans from what the car took.power_max_age_s): (2 s, none), (40 s, 180 s), (120 s, 180 s), (60 s, none). Cloud timings also get the Easee driver's ownautosplit (voltage × the fuse's current) and its 90 s phase hold (easee_cloud.lua pick_phases), and report 10 s late.pickSurplusStepsreads it (here, the export the case will have), so one phase is judged as Core decides it: only when the next 30 min stay below three phases at 6 A.3p, or1pon a 1p charger, asweb/app.jsdoes), surplus-only while export steps 2.5 / 6 / 9 / 3 kW (never past the fuse with the car off), Pause for 25 s then Start, and a planned charge across a 20 s Core restart.Configurations
Settings only: no names, hosts, credentials or addresses. Help-report data stays local; the matrix holds only the charging settings below, with the house rounded to 2 A a phase.
help1022/phase_mode: auto, steps 1,600-3,800 W (one phase) and 4,800-11,100 W (three), min 1,600, max 11,100, 84 kWh, deadline goal,control_interval_s: 5, house about 2 A a phase. Pixii is the site meter; its battery sat at its floor, so it is left out.pr1763/3p, steps 6-15 A on three phases (max 10,350), car takes about 8.3 kW, Pixii charging 6 kW from the plan at the same time, house heavier on L1.homebox//api/loadpointsphase_modeunset, no steps, 4,140-11,000 W, 76 kWh.sweep/auto/3p/1p× steps none (whatloadpoints.jswrites for a new charger: 4,140-11,000 W) / Easee whole amps with one-phase levels / 3p only (1p only for a 1p charger) × car 3p / 1p × house balanced (3 A) / mostly on L1 (7-1-1 A), planned and Charge now, Easee 40 s / 180 s.timing/reports/simulatorprov-20261021) and axesauto, Easee steps) as the rig's s5s, (3p, 3p steps), every request, every timing, house on L1.probe/,help1022/.../car1p,+L1stepauto); unset mode with a goal and a 2,300 W plan slot; surplus-only with a goal and 2.5 kW export; a held 1p order raised to 3p beside L2 at 20 A on 25 A; the help report's site with a one-phase car; a 10 min, 8 A step on L1 under a three-phase car.Pruned: a
1pphase mode is a one-phase charger, so car phases and a 3p table do not apply to it; 20 A runs as the help report's site; duplicate cases run once (twohomebox/rows replace theirsweep/twins).Invariants, every second
not_following(or limited/one_phase) after two minutes and a half, read fromGET /api/loadpointsas the app reads it.Known rows
go/internal/control/testdata/sitesim_field_known.txtlists each row and invariant that fails on master with the fix that turns it green. A listed row that passes fails the test ("passes now: delete its known line"), and so does an unlisted failure.FTW_FIELD_KNOWN=writerewrites the file from a run, keeps each label and marks new failuresnew.FTW_FIELD_TRACE=<case>logs each order with the phases, the offer and the car.Results
On master: 89 of 338 cases pass, 249 fail; all 249 are listed, so the test passes.
Known rows by fix (row = case and invariant), checked by running this matrix on each branch:
AB#1795nettingDexportreadbackreliefhold3pWith #1795 and #1799 together, 241 lines go and 86 cases turn fully green. 20 rows appear: one-phase cars on planned charging move from
one-phasetoenergy(D: the charger now stays on 3p, as it should, and the plan counts 3p watts) or toover-cut(netting). #1799 alone also addsverdictrows that #1795 turns green.What the field reports reproduce
cut the car 3500 -> 0 W for the fuse with every phase under 19 A (meter [15.2 1.7 1.5] A)while planned at a one-phase step underauto. Fixed by A (cut) and B (three phases).autoplanned case with min 4,140 W: under a goal Core sendsauto,evPoolForthen grants one phase of room, and an idle start needs room formin_charge_won that one phase (18 A). A planned charge never starts on 16 A. Fixed by B.autowith no steps, 25 A: Easee splitsautoat 25 A × 230 V = 5,750 W, so a 4,140 W order runs on one phase at 16 A. Core cuts 4,140 → 3,910 → 3,680 W and the current does not move; the phase stays at 25 A past the limit window. Fixed by B.reliefandnetting.Findings no beta.2 fix covers
evSettle(20 s) (ev_fuse_budget.gostepEVMeterLimit,peakW). Easee shows a cut after 30-120 s, so Core cuts again before the first cut lands: 10,400 → 6,900 W, then 0 W 25 s later after a 6 A kettle. Of the 95 double cuts on master, 43 take the car to 0 W, 25 cost one extra step and 27 more. The same window misses a raise still on its way, so a cut is too small and the phase stays over the limit (homebox/.../cloud60s/.../pause). The rule "count our own orders that cannot have shown yet" covers cuts too; the owner decides.fuse_heat.goabs; fix(control): phase current takes its direction from the phase's power #1782).surplus_reserve.go:128.auto, Charge now never starts: the on-box app sendsphase_mode: 3pbecause/api/loadpointsreports 3 phases forauto, and Core floors a 3p order to the table, below 4,140 W, so 0 W with reasonfuse_limit. Planned charging on the same charger works with fix(loadpoint): planned charging runs on three phases, and room never cuts a running order #1799.Overlap with open PRs
#1799 and #1796 also change
sitesim_test.goandsitesim.go. Theplanfield, the boot lines and the Charge now condition here are byte-identical to #1799's, and the sim's Easee split lives insitesim.EVSEinstead ofsend, so this commit cherry-picks cleanly onto #1795, #1796 and #1799. Whichever lands second deletes its known lines withFTW_FIELD_KNOWN=writeand reviews the diff.Runtime
53 s of CPU, 10 s wall on 10 cores; 15 s wall with
GOMAXPROCS=4 go test -p 2. Deterministic over three runs. The matrix skips-racebuilds (//go:build !race): it runs one goroutine per case and took 826 s of CPU under the race detector.TestSiteSimScenariosstill runs under-race.Evidence
go test ./internal/control/ ./internal/sitesim/ -count=1passes,go vet ./...andgo vet -race ./internal/control/are clean.go test ./internal/control/ -run TestSiteSimFieldMatrix -count=3passes.go test -race ./internal/control/ -run TestSiteSimpasses (existing scenarios only).Not in this PR
netting,readbackandDneed an owner decision.🤖 Generated with Claude Code