Skip to content

fix(ocpp): an Available charger is a measured 0 W - #1779

Merged
frahlg merged 5 commits into
masterfrom
fix/ocpp-available-zero-power
Oct 21, 2026
Merged

frahlg merged 5 commits into
masterfrom
fix/ocpp-available-zero-power

Conversation

@frahlg

@frahlg frahlg commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Problem

An OCPP charger with no car sends no MeterValues, and its power only becomes known through MeterValues. StatusNotification Available cleared the power instead of reporting 0 W. Live Home use was therefore estimated (unknown_power:<id>:ev) whenever such a charger was idle, even with every device read, and the day's Home use in history was incomplete. Since #1773 the page shows "≈" exactly when load_quality is estimated, 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/ocpp only. No new setting. No change in telemetry, control or loadpoint.

  • Available means no vehicle, so no current flows. The status records a known 0 W in forecast_power (the existing ForecastPowerSample, Known: true).
  • Any message from the charger (heartbeat, status, Authorize, DataTransfer, meter values and the rest) stamps that 0 W again while the status holds on a live socket, at most once a second. OCPP reports a status change when it happens, so a later message proves Available still holds. A boot, a new socket and messages from the car itself (2.0.1 NotifyEVChargingNeeds/Schedule) do not confirm it.
  • Only a zero is held. If the meter reads more than 0 W while Available, that reading is not stamped again; it ages out.
  • Leaving Available drops the zero at once. Any other status (Preparing, Charging, Suspended, Finishing, Reserved, Occupied), a 1.6 StartTransaction or a 2.0.1 transaction event other than an Idle update makes power unknown until the meter reports. Before, the 0 W stayed measured for up to 90 s while the car drew.
  • Late meter samples. A sample the charger took before the Available cannot bring back the car's draw. The gate uses the status time the charger sent, so a charger whose clock runs behind Core's does not lose the next session's first samples. Without a status time (optional in 1.6) the gate stays at the last meter sample. The gate has its own field (meterAtMS), so stamping the zero on Core's clock never moves it.
  • A stop after Available (some chargers send them in that order) keeps the 0 W.
  • Unavailable and Faulted stay unknown: the charger may be broken.
  • Heartbeat default 30 s (was 60 s), the interval Core asks for in the BootNotification reply. An idle charger often sends nothing but heartbeats, and both the watchdog and history's source rows allow 60 s, so a 60 s heartbeat left no margin. Only the default changes: ocpp.heartbeat_interval_s stays 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 and docs/ocpp.md show 30.
  • Connector 0 (1.6) is the charge point itself. Its Available, sent at boot and on reconnect, no longer counts as a cable status. It used to set connected=false and 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 use measured, 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 is measured with 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.
  • Review probes, each failing on 27a15a6:
    • 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_power and 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 through SaveAtomic, and a saved 60 stays 60.
  • TestOCPPUnavailableAndFaultedStayUnknown: both stay unknown_power, also after a heartbeat.
  • TestHistoryCountsAnEmptyOCPPChargerAsMeasured (cmd/ftw): a real ocpp.Handler behind buildHistoryPoint. 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 reports time_skew (BalanceMismatch), and the chart points from LoadHistoryContext still carry the source rows' quality, not balance_mismatch. Fails if applySourceLoadHistory keeps the aligned flags.
  • TestOCPPLastPowerSharedAcceptRule asserted that Available is not a measured zero. It now asserts the opposite.

make verify passes locally on the head with master merged in (GOFLAGS=-p=2).

Simulator (srcfl/device-simulator 3965d8e, scenario s0-25)

Ran reports/simulatorprov-20261021/rerun.sh on its own ports (+10 000), because other sessions held the default ones. For the head run the rig's FTW config leaves heartbeat_interval_s unset, so the default applies (the stock rig pins 60). 129 status samples over 150 s, all devices read.

master fc621be head 32d6ffb (master 41675e4 merged in)
Result fails passes
Live load_quality estimated 129/129 measured 129/129
load_reason unknown_power:OCPPSIM02:ev none
EV in status null 0 W
Today's Home use incomplete, no value estimated, 79 Wh
Source rows (history_source_energy) EV and Home use missing nothing missing
OCPP frames from Core 1 GetConfiguration the same
Heartbeat asked / sent 60 s / every 60 s 30 s / every 30.0 s

The simulator sends connector 0 Available before connector 1 at boot, so the run covers that order. estimated is 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 report time_skew instead of unknown_power. It feeds the partial dashboard rows and the planner's live-load overlay. The chart does not show it: in the source era applySourceLoadHistory replaces 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 balance MissingLoad for another reason, which this PR does not address.

🤖 Generated with Claude Code

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>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T12:52:14.385182Z f49642d PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread go/internal/ocpp/handlers.go Outdated
Comment on lines +767 to +770
if !s.available || !s.online || !s.connectedKnown || !s.forecastPower.Known || ms <= s.forecastPower.MeasuredAtMS {
return false
}
s.forecastPower.MeasuredAtMS, s.forecastPower.ReceivedAtMS = ms, ms

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread go/internal/ocpp/handlers.go Outdated
ms := now.UnixMilli()
s.available = true
s.lastPowerW = 0
s.meterAtMS = max(s.meterAtMS, ms)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
frahlg and others added 3 commits October 21, 2026 16:37
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>
@frahlg
frahlg merged commit 98e5c79 into master Oct 21, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant