Repository navigation
fix(control): one log of FTW's own EV offers - #1881
Merged
Merged
Conversation
The battery side read a 20 s pool log of EV offers (State.evOffers), while the EV fuse limit keeps each charger's offers for its declared response time, or 180 s when it declares none. After 20 s the battery counted a slow charger's unshown raise as already in the meter and charged into that room; when the car started, a 25 A site read 32.6 A per phase for 3 s. Delete State.evOffers and its two writers. evInMeterW and clampEVBatteryHeadroom read evShownW, each charger's latest offer less what it may not show yet. The hourly peak in stepEVMeterLimit takes each charger's highest offer of the last evSettle from the same log. 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. |
The first A6 change gave the battery side the EV limit's 180 s guess for every charger that declares no response time. Only Easee Cloud declares one, so on every other charger the battery reserved the car's whole demand on top of a reading that already showed the car, and stopped charging for 3 minutes after each car start or raise. On the #1868 shape (rig s18) battery charge stayed at 0 W until 180 s. evShownW now uses the charger's declared power_max_age_s, and evSettle when it declares none, as master did. Easee Cloud declares 180 s, so the slow-charger case stays fixed. Tests: the slow-charger test now declares 180 s, and a new plant test runs the s18 shape with a fast charger that declares nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg
added a commit
that referenced
this pull request
Oct 27, 2026
#1881 (the old base) squash-merged. Conflicts kept the branch's fuseLineA() log line and sitesim meterNoise field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Item A6 of the stable plan.
Problem
Core kept two logs of its own EV offers:
State.evOffersheld 20 s. It was written at ev_fuse_budget.go:372 and dispatch.go:2478.evLimit.offers) holds the declared response time, or 180 s when the charger declares none.The EV fuse limit reads the per-charger log. The battery side read the 20 s log, through
evInMeterW(dispatch.go:4649) andclampEVBatteryHeadroom(ev_battery_headroom.go:20). After 20 s, the battery counted a slow charger's raise as already in the meter and charged into the car's room.Scenario: a 25 A site with 3 A of house load per phase, a planned 9.4 kW battery charge, and an 11 kW Charge-now car on a charger that starts after 2 min. The battery ramped to 9.4 kW while the car's offer was unshown. When the car started, all three phases read 32.6 A for 3 s, above the 30 A instant limit (1.2 × In). The car then sat idle with room for 3 min. A 30 s or 40 s start gave 32.0 A.
Change
State.evOffersand both of its writers.evShownW: each plugged charger's latest offer less what it may not show yet (commandLog.unshown).evInMeterWandclampEVBatteryHeadroomread it.power_max_age_skeeps a raise unshown for that time, as the EV limit does. Easee Cloud, the slow charger we know, declares 180 s, so the battery now waits for its raise.peakWinstepEVMeterLimitsums each charger's highest offer of the lastevSettlefrom the same per-charger log. I kept the 20 s window here, not each charger's response time. The hourly import cut needs it: with the longer window,TestEVHardLimitCutsAgainAfterSettleWhenChargerIgnoresandTestEVHardLimitCarUnderItsOfferDelaysCutfail, as the audit found.sent(st, "ev", …)now seeds the per-charger log.issue1868Siteseeds the car's 4140 W offer under its loadpoint ID and older than 180 s: the steady state Fuse guard sheds EV fully (min step 4140 W) while home battery grid-charges, and EV never restarts #1868 is about. The young offer has its own test below.power_max_age_soutside the field matrix (simSite.evMaxAge).Why undeclared chargers keep 20 s on the battery side
The first version of this PR gave the battery side 180 s for every charger that declares none. Only Easee Cloud declares a response time, so every other charger (CTEK, Zap/OCPP, Ambibox, Ferroamp, Tesla Wall Connector) got 180 s. The battery clamp reads only the site meter and our own commands. For 180 s after each car start or raise it then reserved the car's whole demand on top of a reading that already showed the car. On the #1868 shape (rig s18: 16 A, 3 A house, car steps [0, 4140] W, 6000 W battery charge, a charger that draws within 2 s) battery charge stayed at 0 W from the car's start until 3 min, where master charges at 4140 W from about 60 s. The s18 judge needs the battery above 1000 W at 100-120 s. A review caught this.
The cost of the 20 s rule is the case this PR started from, but only for a slow charger that declares nothing. The one slow controllable charger in the bundled set declares its time, and Zaptec Cloud is read-only. A slow charger added later should declare
power_max_age_s.Other fixes the review listed, not taken here:
Tests
TestSlowChargerUnshownOfferKeepsBatteryOut(from the audit reprosTestReproSlowCharger*in caa-165 andTestReproEVBatteryStagesSlowChargerin caa-173): a charger that declares 180 s, with start delays of 5 s, 30 s, 40 s and 2 min.TestFastChargerYoungOfferLetsBatteryShare(the review's s18 plant test), with a manual battery hold and with a planned charge: car at 4140 W and battery at 3000 W or more at 60, 100, 120 and 170 s. With the first version's 180 s rule it fails: battery 0 W at 60 s.cd go && go test -race -count=1 ./internal/control/... ./internal/loadpoint/... ./internal/sitesim/...: ok (control 205 s).go vet ./internal/control/andgo build ./...: ok.make verify. The siterig runner and CI cover them.Risk
Medium. This changes how much EV load the battery and the joint allocator treat as already in the meter.
🤖 Generated with Claude Code