Repository navigation
fix(control): an idle car starts on the hour's slack under the import ceiling - #1828
Merged
Merged
Conversation
… ceiling The hourly import ceiling caps each clock hour's mean import, but the idle start held the car to the ceiling itself: evMeterRoom set the start level to min(rise level, ceiling), and stepEVIdleStart needs that level less the dwell's mean import to hold the car's minimum. When house load plus the minimum sat above the ceiling, the car never started, however much of the hour was left. A help report showed a 4.5 kW ceiling, 0.85–1.3 kW of house load and a 4,140 W minimum: 0 W to the car for a whole hour with 3.3–3.9 kWh of room. The start level is now the rise level, which already follows the hour's rules: it starts each hour at the ceiling, rises in whole steps with one in hand, waits 5 minutes after a cut and does not rise in the last 15. The rate cut still guards the hour's energy. In the hour's last quarter the start level stays at most the ceiling, as before, so Core never orders a start on slack with less than 15 minutes of the hour left. Tests: TestImportCeilingNoStartOnSlackAlone becomes TestImportCeilingStartsOnHoursSlack, with the help report (850 and 1,300 W house, 4.5 kW) and the old 5 kW / 7.5 kW case. Over three hours the car starts in every hour, stops at most once in each and never within 15 minutes of a start, every hour's mean holds the ceiling, and every tick says import_limit. TestImportCeilingNightNeverSaysFuse now lets the car run on the 22:00 hour's slack and checks the label from 23:01 to midnight. TestImportCeilingStartTestSaysImportLimit pins the last-quarter rule. Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com> 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. |
frahlg
added a commit
that referenced
this pull request
Oct 25, 2026
#1837) * test(site): an import ceiling and two equal batteries in the site pack s16 runs Charge now under a 4.5 kW hourly import ceiling with house and the car's minimum above it (#1828). s17 runs two equal batteries under Energyplan (#1814). FTW_SITE_ENGINE picks the planner for a whole run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com> * test(site): s17 waits for a quarter the plan discharges Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com> * test(site): s17 plans 6 h ahead so the plan discharges now Over 48 h Energyplan keeps both batteries for tomorrow's dearer evening and the plan's current quarter stays idle, so Core's per-battery path never ran. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com> --------- Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com> 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.
Problem and result
A beta user's help report (25 October 2026) has the hourly import ceiling on:
peak_import_ceiling_w4,500 W, hourly-average mode. The car's minimum is 4,140 W and the house used about 0.85–1.3 kW. For a whole hour the car stayed at 0 W (fuse_cap_w=0, reasonimport_limit), although the hour had 3.3–3.9 kWh of room left.The ceiling caps the clock hour's mean import, but an idle car could start only if house load plus its minimum fit under the ceiling at that moment. Here 0.85–1.3 kW + 4.14 kW is above 4.5 kW, so the car never started. A car that was already charging would have been allowed to keep going under the hour's rate and rise rules.
Now the idle car starts once the hour has saved enough room. It charges to the end of the hour, the next hour starts again at the ceiling and stops it, and every hour's mean stays under the ceiling. Owner request: "fix all bugs".
Cause.
go/internal/control/ev_fuse_budget.go:575(master) setr.startLevelW = math.Min(riseW, s.PeakImportCeilingW).stepEVIdleStart(:532–536) needsstartLevelW − dwell's mean import − pending ≥ minimum, so for a start the hourly average worked as an instant cap. The rule came with #1699 ("on the hour's slack alone it would start at about :35 and stop at :00, once an hour"), andTestImportCeilingNoStartOnSlackAlonepinned it.Change (
ev_fuse_budget.go:573–579):riseW. The rise level already follows the hour's rules (stepHourImportLimits,import_hour.go:149–188). It starts each hour at the ceiling, rises only in whole steps with one step in hand, waits 5 minutes after a cut and never rises in the hour's last 15 minutes. The rate cut still guards the hour's energy.importHourRiseMinLeft) the start level stays at most the ceiling, as before. A start on slack then would run only minutes before the next hour stops it. A car that fits under the ceiling itself still starts then.Scope and safety
riseW ≤ limitWalways holds, and the cut on the hour's rate is unchanged, so a start cannot overrun the hour. A Core restart, a new ceiling or a new hour puts the rise level back at the ceiling. A meter gap never raises it. Before the hour is measured, the start test does not read the start level.import_limit, whether idle or charging below its want. When the fuse leaves less room, it saysfuse_limit, as before.ev_fuse_budget.goandimport_hour*.go:stepEVIdleStartand theevMeterRoomconstructor. It keeps the oldstartLevelWline and comment. On a rebase, keep this PR's start level and its last-quarter rule.evMeterRoomfield comments, not the start level.offerPhaseAandevPoolForonly.loadpoint/controller.goandsitesim_test.go, not this file.Limits
fuse_limitfor about 6 s. The label names whichever room is smaller, and the fuse's heat model briefly leaves less.TestImportCeilingNightNeverSaysFusechecks the label from a minute after the stop.web/app.js:3001says "this hour's import budget is used" for an idleimport_limit. That is now also shown while the car waits for the hour to save room. The wording is not changed here, because a UI change needs a browser review. Follow-up.wantsCharge(sitesim_test.go:400–405) expects a start once house + minimum fits the hour's rate. Core's rise level trails that rate by one or two steps. No current row combines a ceiling with house + minimum above it, so nothing fails. A future row would need that slack.Verification
go/internal/control/import_hour_test.go, thehourSiteharness: an Easee-like 3-phase charger (5 s command delay, 10 s late telemetry), a 25 A site, a 1 s site meter, 4,140 W minimum.fuse_limit.import_limitTests:
TestImportCeilingNoStartOnSlackAlonebecomesTestImportCeilingStartsOnHoursSlack, with the first three rows. Over three hours, in every clock hour:import_limit.TestImportCeilingNightNeverSaysFuseis the fourth row.TestImportCeilingStartTestSaysImportLimitis the fifth row and pins the last-quarter rule.Mutation checks:
math.Min(riseW, ceiling),TestImportCeilingStartsOnHoursSlack(all three cases) andTestImportCeilingNightNeverSaysFusefail.TestImportCeilingStartTestSaysImportLimitfails: the car starts at 21:46:01.Runs:
go test ./internal/... ./cmd/ftw/... -count=1passes, with drivers fetched bymake drivers.go vet ./internal/... ./cmd/ftw/...is clean.go test -race ./internal/control/ -count=1passes.go test ./internal/control/ -run TestSiteSimFieldMatrix: 340 cases, 29 fail as known (idle 23, own-raise 6).testdata/sitesim_field_known.txtis unchanged. No matrix row sets the ceiling, so this change cannot move it. The ceiling scenario insitesim_test.go("end of a heavy hour, low phases") still passes.SeedHourImport, meter gaps, a ceiling changed mid-hour, zones and battery pending orders, and found no bug. Its random-load runs (house spikes, Core restarts, gaps; 80 and 96 car-hours at 4.5 and 7.5 kW) had no hour above the higher of the ceiling and the house-only mean. Its wording points are fixed in the changeset and the commit message. The rest are listed under Limits.Checklist
patch).🤖 Generated with Claude Code