Repository navigation
fix(ocpp): an Available charger is a measured 0 W - #1779
Conversation
An OCPP charger with no car sends no MeterValues, so its power stayed unknown and live Home use was marked estimated on most OCPP sites; the day's Home use in history was incomplete for the same reason. Available means no vehicle, so no current flows. The status now records a known 0 W in forecast_power, and each heartbeat confirms it while the status holds on a live socket. A newer meter sample still wins; one from before the status cannot bring back the car's draw. Unavailable and Faulted stay unknown. Preparing, Suspended and Finishing keep the last sample, which ages out as before. The meter-time gate moves to its own field so a heartbeat on Core's clock cannot reject the next session's samples from a charger whose clock runs behind. 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. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f49642db3b
ℹ️ 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".
| if !s.available || !s.online || !s.connectedKnown || !s.forecastPower.Known || ms <= s.forecastPower.MeasuredAtMS { | ||
| return false | ||
| } | ||
| s.forecastPower.MeasuredAtMS, s.forecastPower.ReceivedAtMS = ms, ms |
There was a problem hiding this comment.
End the Available hold when a newer nonzero meter sample wins
If a newer MeterValues sample arrives after Available, recordPower accepts it but leaves available true. Subsequent heartbeats therefore refresh that sample's timestamps even when its watts are nonzero. A single nonzero reading can remain qualified as fresh indefinitely without further power measurements, corrupting live Home use and recorded source energy. Clear the hold when a nonzero meter sample supersedes the inferred zero, or restrict heartbeat confirmation to the held Available zero.
Useful? React with 👍 / 👎.
| ms := now.UnixMilli() | ||
| s.available = true | ||
| s.lastPowerW = 0 | ||
| s.meterAtMS = max(s.meterAtMS, ms) |
There was a problem hiding this comment.
Keep the meter timestamp gate on the charger's clock
For a charger whose clock runs behind Core, assigning Core's current time to meterAtMS rejects valid measurements from the next session. For example, on an established socket with a 30-second clock offset, charging that starts ten seconds after Available produces power samples timestamped before this gate; recordPower discards them and continues publishing the inferred 0 W until the charger clock catches up. Clearing available on transaction start does not clear the gate. Keep the Available ordering guard separate from the meter-clock watermark so a new session can accept its measurements.
Useful? React with 👍 / 👎.
In OCPP 1.6 connector 0 is the charge point itself. Its Available, sent at boot and on reconnect, said the whole charger had no car: it set connected=false and cleared power, and with the held 0 W it would also have claimed a measured zero while connector 1 charged. It now changes neither. Unavailable and Faulted on connector 0 still cover the charger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1779: - A heartbeat re-stamped whatever the meter last read. It now confirms only a 0 W reading; a higher reading ages out. - The gate against late meter samples used Core's clock, so a charger whose clock runs behind lost the next session's first samples. It now uses the status time the charger sent, and stays put without one. - Leaving Available (any other status, or a transaction other than an Idle 2.0.1 update) drops the 0 W to unknown at once instead of letting it age for up to 90 s while the car draws. - Any message from the charger confirms the zero, not only heartbeats. Boot, a new socket and messages from the car itself do not. A test pins that the chart keeps the source rows' quality when the time-aligned balance reports time_skew for an ageing zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An idle charger often sends nothing but heartbeats. The watchdog and the history source rows both allow 60 s, so a 60 s heartbeat left no margin: one late beat marked the charger offline and its Available 0 W stale, and training dropped about half of all quarter-hours at 100 ms jitter. Only the default changes. An unset ocpp.heartbeat_interval_s stays unset through a save; an explicit value, 60 included, is kept. Traffic: one more small Heartbeat request and reply a minute per charger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
An OCPP charger with no car sends no MeterValues, and its power only becomes known through MeterValues.
StatusNotification Availablecleared the power instead of reporting 0 W. Live Home use was thereforeestimated(unknown_power:<id>:ev) whenever such a charger was idle, even with every device read, and the day's Home use in history wasincomplete. Since #1773 the page shows "≈" exactly whenload_qualityisestimated, so most OCPP sites now see "≈" most of the time. Found in the end-to-end simulator run of 21 Oct (scenario 0, finding 3).Change
go/internal/ocpponly. No new setting. No change intelemetry,controlorloadpoint.forecast_power(the existingForecastPowerSample,Known: true).NotifyEVChargingNeeds/Schedule) do not confirm it.meterAtMS), so stamping the zero on Core's clock never moves it.ocpp.heartbeat_interval_sstays unset through a save and an explicit value, including 60, is kept. Traffic: one more small Heartbeat request and reply a minute per charger. The example configs anddocs/ocpp.mdshow 30.connected=falseand clear power for the whole charger; with the hold it would also have claimed 0 W while connector 1 charged. Unavailable and Faulted on connector 0 still apply to the whole charger. Reported by the Charge now session for fix(loadpoint): Charge now keeps charging after a restart #1778, which does not touch this file.Assumption: one connector
Charger state is per charger, not per connector, as before this PR. On a two-connector charger, an Available on connector 2 while connector 1 charges claims 0 W for the charger until connector 1's next meter sample, and also sets
connected=false(that part is old). FTW's loadpoints model one connector per charger today.Tests
TestOCPPAvailableIsAMeasuredZeroInBothVersions(1.6 and 2.0.1): Available after boot with no MeterValues gives live Home usemeasured, EV 0 W, a valid forecast balance and 0 W for dispatch. Two minutes without a message makes it unknown; a heartbeat makes it current again. A car arriving or a reconnect ends it.TestOCPPAvailableAfterSessionIsAMeasuredZero: charging with MeterValues ismeasuredwith the meter's watts. Available after the session, in either order with the stop, gives a measured 0 W. A late sample from before Available is rejected. An Idle 2.0.1 update keeps the zero.TestOCPPAvailableConfirmsOnlyAZero(probe 1): Available, a 3000 W sample, 2 min silence, heartbeat: not measured.TestOCPPAvailableGateFollowsTheChargerClock(probe 2): charger clock 30 s behind, car right after Available: its 3000 W sample is accepted and measured, with or without a status time.TestOCPPZeroEndsWhenTheCarArrives(probe 3): Available, then Charging/Occupied or a transaction start:unknown_powerand no valid training balance until the meter reports.TestOCPPAnyMessageConfirmsAvailable: Authorize confirms an aged zero; a boot does not.TestOCPPChargePointAvailableIsNotACableStatus: connector 0 Available alone leaves cable and power unknown, leaves a charging car connected at 3000 W, and does not end the zero from connector 1.TestHistoryKeepsAnIdleOCPPChargerMeasuredBetweenHeartbeats(cmd/ftw): ten heartbeats at the default interval ±1 s through the real handler, 2 s history ticks at shifting phase, the charger's reading and health aged by the time since each beat. Every source row is complete and the range's Home use is known. The same run at 60 s ±1 s loses ticks, which the test also requires.TestHeartbeatDefaultIsHalfTheWatchdog(ocpp): unset gives 30 s in the BootNotification reply; 60 and 120 are kept.TestOCPPHeartbeatSurvivesSave(config): unset stays unset throughSaveAtomic, and a saved 60 stays 60.TestOCPPUnavailableAndFaultedStayUnknown: both stayunknown_power, also after a heartbeat.TestHistoryCountsAnEmptyOCPPChargerAsMeasured(cmd/ftw): a realocpp.HandlerbehindbuildHistoryPoint. Source rows have no missing flow and the day's Home use is known. Fails on master.TestHistoryChartKeepsSourceQualityForAnAgeingOCPPZero(cmd/ftw): with a 0 W 40 s old, the time-aligned balance reportstime_skew(BalanceMismatch), and the chart points fromLoadHistoryContextstill carry the source rows' quality, notbalance_mismatch. Fails ifapplySourceLoadHistorykeeps the aligned flags.TestOCPPLastPowerSharedAcceptRuleasserted that Available is not a measured zero. It now asserts the opposite.make verifypasses locally on the head with master merged in (GOFLAGS=-p=2).Simulator (srcfl/device-simulator 3965d8e, scenario
s0-25)Ran
reports/simulatorprov-20261021/rerun.shon its own ports (+10 000), because other sessions held the default ones. For the head run the rig's FTW config leavesheartbeat_interval_sunset, so the default applies (the stock rig pins 60). 129 status samples over 150 s, all devices read.load_qualityestimated129/129measured129/129load_reasonunknown_power:OCPPSIM02:evnullincomplete, no valueestimated, 79 Whhistory_source_energy)The simulator sends connector 0 Available before connector 1 at boot, so the run covers that order.
estimatedis the best quality the source-era day total gives; scenarios with a charging car reach the same label.Known limits
Margin. Fixed by the 30 s default for installs that do not set the interval. A box whose config says
heartbeat_interval_s: 60(some copied it from the old example) keeps 60 and keeps the zero margin: a heartbeat d ms late leaves the zero stale for d ms, d/2000 of 2 s history ticks a minute, and can mark the charger offline for the same moment. Chargers that ignore the interval Core asks for are not helped either.Time-aligned balance.
ForecastMeasurementNow(max skew 30 s) sees the zero up to one heartbeat old, so near the end of each interval it can reporttime_skewinstead ofunknown_power. It feeds the partial dashboard rows and the planner's live-load overlay. The chart does not show it: in the source eraapplySourceLoadHistoryreplaces the aligned rows' Home-use flags (MissingLoad, BalanceMismatch) with the source rows' evidence, so idle OCPP sites see no "Measurements do not balance" (pinned by the test above). Day totals and load training use source rows. The live status uses the unaligned balance. In the rig every case, even with a charging car, also flags this balanceMissingLoadfor another reason, which this PR does not address.🤖 Generated with Claude Code