Repository navigation
fix(ocpp): a managed charger starts at 0 A on boot - #1911
Merged
Merged
Conversation
Until FTW's first order lands, an OCPP charger runs at its own default, usually its rating. In random rig seed 207534 the charger booted at 5.0 s, a car plugged in at 7.3 s and drew 11040 W from 10 s until FTW's first 0 A took effect at 13.5 s, with 18.9 A on a 16 A fuse. On BootNotification (1.6 and 2.0.1) from a charger a loadpoint names, send the same 0 A TxDefaultProfile FTW sends to pause: profile id 1, stack level 0, connector or EVSE 0, with the usual connector-1 retry. FTW's first order has the same id, purpose and stack level, so it replaces the boot profile. Skip chargers listed only under ocpp.chargers (FTW never orders them), pending chargers, a boot that answers FTW's own identity probe, and a boot on a socket that already reported a car. A refusal is logged once and not retried until the next boot. 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. |
Setup measurement mode blocks every charger write, standdowns included. The boot limit went straight to setLimit, so a charger named by a loadpoint saved during setup got 0 A and kept it until restart. Both the start and the hot-reload paths now pass no managed chargers while Core runs in the mode it booted in. Much firmware sends its own BootNotification on every reconnect. The car guard read only the new socket, so a Wi-Fi drop mid-session paused the car. The guard now reads the last status FTW holds, which survives the reconnect, and any open transaction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 27, 2026
Rig s7s-restart-short-25A failed twice on 964643e: after a Core restart the charger sent its own BootNotification at once, the new process held no status or transaction, and FTW sent the 0 A start limit mid Charge now. The car went to SuspendedEVSE for a second. A boot now only marks the charger. The first connector status after it decides: no car and no transaction sends the 0 A limit; a car sends nothing. Connector 0 Available on 1.6 does not decide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 0 A start limit goes to connector or EVSE 0, the whole charger. It went out on the first connector status after a boot that showed no car. On a two-socket 1.6 charger or a two-EVSE 2.0.1 station, a Core restart while a car charged on socket 2 sent 0 A as soon as socket 1 reported Available, and paused that car. FTW now waits until the statuses after the boot have been quiet for one second. Any status that shows a car or a transaction cancels the limit as it arrives, since the last status wins in FTW's one state per charger. An order from FTW during the wait also cancels it, so the 0 A cannot land after that order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Start sets the settle wait next to bootLimit instead of in NewHandler, which keeps this branch from touching the line #1787 changes there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a Core restart a two-socket 1.6 charger may report socket 1 Available and then send socket 2's meter values for its running transaction before socket 2's status. Without #1787 FTW holds no transaction id then, so the 0 A start limit went to the whole charger and paused that car. A 1.6 meter value that names a transaction or measures power above zero now cancels the pending limit, as a status with a car does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 tasks
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
Item C9 from the 27 October random rig triage (master 83c2b24). Until FTW's first charging profile lands, an OCPP charger runs at its own default, which is usually its rating. FTW orders a plugged-in car every tick, but the first order still takes one tick plus the charger's response time.
Seed 207534 (
house/plant.jsonl,house/ocpp-garage.jsonl,ftw.log), in house elapsed seconds:BootNotificationfrom the charger. FTW sends no profile.offer_w11040.Preparing).offer_wdrops to 0.The same pattern gives 18.9 A peaks in 207537. With a 0 A profile sent at boot, the 5 s charger in 207534 would have held 0 A about 3.5 s sooner; with the one-second settle wait added in e6fc34b (below), about 2.5 s sooner. This does not help the 40 s chargers in 207532 and 207537. Those need the rig fix (R4).
Rig s7s on 964643e
siterig/short failed twice on
s7s-restart-short-25A("Charge now never stops"):~/siterig/results/20261027-232851-964643e9-shortand~/siterig-pr/results/20261028-003905-964643e9-short. Master and other PRs pass s7s. The scenario restarts Core for 20 s during Charge now. Fromhouse/ocpp-garage.jsonl, seconds after the first boot:BootNotificationat once, before FTW's identity probe.Availableand connector 1Charging.SuspendedEVSE. The car drops to 0 W.Charging.The new process held no status and no transaction, so the boot alone looked like a fresh start. The first version of this PR listed this case under Risk as a one-tick pause in the safe direction; the rig counts it as Charge now stopping, and it is right to.
Change
BootNotification(1.6 and 2.0.1), FTW waits for the charger's connector statuses. Once they have been quiet for one second (defaultBootLimitSettle) and none showed a car or an open transaction, FTW sends a managed charger the 0 A profile it already sends to pause:TxDefaultProfile, profile id 1, stack level 0, connector or EVSE 0, with the existing retry on connector 1. A 1.6 charger reports each connector after an accepted boot (§4.9); 2.0.1 stations sendStatusNotificationthere too. On 1.6, connector 0Availabledoes not count, since it says only that the controller is up. The new code is ingo/internal/ocpp/boot_limit.go:maybeSendBootLimitmarks the boot,resolveBootLimitruns on each status,fireBootLimitsends when the wait ends. It reusessetLimit, so it shares the one-profile-at-a-time slot with FTW's orders.Available, and that car paused. That is the s7s fault on another charger shape. Now:ev_set_current,ev_pause,ev_startorev_resumefrom FTW during the wait cancels it (cancelBootLimitinServer.Command), so the 0 A cannot land after FTW's own order.Availableand socket 2 may then send the meter values of its running transaction before its own status. Without fix(ocpp): take up a running 1.6 transaction after a Core restart #1787, FTW holds no transaction id at that point, so the 0 A went to the whole charger and paused that car. A 1.6MeterValuesthat names a transaction, or that measures power above zero, now cancels the pending limit as it arrives (meterCancelsBootLimitLockedinboot_limit.go, two call lines inOnMeterValues). On 2.0.1 a running transaction reports throughTransactionEvent, which already marks the car and blocks the send.ocppManagedIDsingo/cmd/ftw/ev_send.go, passed asocpp.Config.ManagedIDsand updated on hot reload. A charger listed only underocpp.chargersis approved and metered, but FTW never orders it, so a 0 A start would hold it at 0 A for good. FTW leaves it alone, and pending chargers too.BootNotificationon every reconnect, before any status, so after a Wi-Fi drop the new socket alone says nothing about the car. FTW orders that car every tick.ocppStartLimitIDspasses no managed chargers, at start and on hot reload. It reads the mode Core booted in, because leaving it takes a restart.handlers.gogets four call lines, five struct fields and two lines that clear the mark on connect and disconnect, apart from the lines fix(ocpp): take up a running 1.6 transaction after a Core restart #1787 changes.Startinserver.gosets the wait, notNewHandler, whosenextTxIDline fix(ocpp): take up a running 1.6 transaction after a Core restart #1787 changes. The branch merges cleanly with fix(ocpp): take up a running 1.6 transaction after a Core restart #1787 (git merge-treeon 797f806). This fix does not need fix(ocpp): take up a running 1.6 transaction after a Core restart #1787: the status after the boot shows the car, and a meter value for a running transaction cancels the limit even when that status comes late.Tests
Each test below fails on master and passes on this branch. Before the fix, no profile arrives at boot and the simulator keeps its rating:
go/cmd/sim-ocpp/ocppcp/boot_limit_test.goTestChargerHoldsZeroAmpsFromBootUntilFTWOrderruns FTW's Central System against the charger simulator foreasee-charge-up(1.6),charge-amps-aura(1.6, refuses connector 0) anddefa-power(2.0.1). In each case:go/internal/ocpp/boot_limit_test.go:TxDefaultProfileon connector or EVSE 0, with id 1 and stack 0. FTW's order uses the same id, purpose, stack level and connector, at 16 A.ocpp.chargersand a pending charger get no profile.Charginggets no profile.TestReconnectBootWithNoCarGetsBootLimit).TestReconnectBootMidSessionKeepsFTWOrder: the charger reportedChargingand took FTW's 16 A, then reconnected and booted before any status. It gets no profile. With the first version of this PR it got 0 A.TestCoreRestartBootMidSessionGetsNoBootLimit(1.6): a new Server, the charger boots on its own, then reports connector 0Availableand connector 1Charging. On 964643e it got 1 profile at 0 A; now none.TestV201CoreRestartBootWithCarGetsNoBootLimit(2.0.1): the same withOccupied.TestManagedChargerStartsAtZeroAmpsOnBootalso checks that the boot alone sends nothing. The simulator test sends the simulator'sAvailableafterBoot();Sim.Bootitself is unchanged, so the e2e tests see the same frames as before.go/cmd/ftw/ev_send_test.go:TestOCPPManagedIDsAreLoadpointChargersOnly.TestOCPPStartLimitIDsFollowMeasurementModeAtBoot: no charger gets the start limit when Core starts in measurement mode, nor on a reload that has left the mode before the restart.go/internal/ocppuse a 50 ms wait sosettle()(300 ms) covers it; the simulator test uses the real one second.TestDualSocketCoreRestartCarOnSocket2GetsNoBootLimit(1.6: boot, then connector 0Available, 1Available, 2Charging) andTestV201TwoEVSECoreRestartCarOnEVSE2GetsNoBootLimit(2.0.1: EVSE 1Available, then EVSE 2Occupied) fail on 92104f5 with "got 1 profile(s), last 0 A; want none" and "got 1 profile(s); want none", and pass now.TestDualSocketCarReportedFirstStillCancelsBootLimit(socket 2Chargingbefore socket 1Available) passes on 92104f5 too. It pins the cancel on arrival: with that cancel removed, so only the timer checks, it fails with 1 profile.TestOrderDuringSettleWaitCancelsBootLimit: FTW orders 16 A right after theAvailable. With thecancelBootLimitcall removed it fails 3 of 3 runs with "2 profile(s), last 0 A"; with it, FTW's 16 A is the only profile.TestMeterValuesShowingACarDuringWaitCancelBootLimit: boot, connector 0 and 1Available, thenMeterValueson connector 2 in three cases: transaction and 7 kW (the reviewer's shape), transaction at 0 W, and 7 kW with no transaction. All three fail on a814fb4 with "got 1 profile(s), last 0 A; want none" and pass now, 3 of 3 under-race.TestIdleMeterValuesDuringWaitKeepBootLimit:MeterValuesat 0 W with no transaction still gets the 0 A limit. It passes before and after, and guards against cancelling too often.TestReviewNoCarStatusRestartsTheWait,TestReviewV201TransactionUpdateDuringWaitBlocksBootLimit,TestReviewMeterValuesWithTransactionDuringWait) pass 3 of 3 under-raceon 797f806. I ran them from a temporary copy and did not commit them, since the new tests cover the same shapes.go vet ./internal/ocppare clean.go test -racepasses on./internal/ocpp/...,./cmd/sim-ocpp/...and./cmd/ftw.make verifypasses (Docker steps skipped, no daemon). My firstmake verifyrun failed in one test I did not capture;make testandmake verifythen passed in full. The knownTestLaterRunsAfterTheSlotIsFreeflake fails on master too. The repo-hygiene scripts pass against 7840c33 (check-no-planning-docs.sh;check-telemetry-reads.sh, 134 reads, none added;test_telemetry_reads;test_verify). On a local merge of 797f806 with fix(ocpp): take up a running 1.6 transaction after a Core restart #1787 (92230c0, clean),go test -race ./internal/ocpp/...passes.make verify, theinternal/ocppand sim-ocpp race tests, the e2e subset and the hygiene scripts ran again there):go test -raceon./internal/ocpp/...,./cmd/sim-ocpp/...,./cmd/ftwand./internal/loadpointpasses, the boot-limit tests pass 3 of 3 under-race,FTW_E2E=1 go test ./test/e2e -run 'Evify|OCPP|Ocpp'passes,make verifypasses (Docker step skipped, no daemon), and the repo-hygiene scripts pass against 7840c33 (check-no-planning-docs.sh;check-telemetry-reads.sh, 134 reads, none added;test_telemetry_reads,test_verify). I did not run TestSite or siterig, and I touched no real devices. No siterig result exists yet for 92104f5 or later; s7s-25 should pass, since the charger reports connector 1Charginginside the wait.Risk
Preparing, which cancels the limit, and FTW's own order covers it, as on master. A charger whose statuses arrive more than a second apart is treated as two bursts; a car on a socket that reports after the limit went out is paused until FTW's next order.Chargingafter its boot, so FTW sends nothing.Availableas 0 W for both connectors, as on master; that is outside this item.Merge order
Independent of #1787. Either can merge first; they merge cleanly together, and the
internal/ocpprace tests pass on the merge. Since 797f806, this PR no longer needs #1787 for the case where a running transaction shows only through meter values.🤖 Generated with Claude Code