feat(ocpp-cp): honor v201 TriggerMessage(FirmwareStatusNotification) by re-reporting the latest firmware status (M7) - #586
Merged
Conversation
…by re-reporting the latest firmware status (M7) The OCPP 2.0.1 CP simulator classified `TriggerMessage(FirmwareStatusNotification)` as `NotImplemented`, even though it models firmware updates (`UpdateFirmware` drives an async `FirmwareStatusNotification(Downloading → … → Installed)` stream). A real station answers this trigger with its *current* firmware status — exactly as the 1.6J side already does via its `firmware_status` field. Wire the v201 twin (Issue #583). The v201 side couldn't before because nothing persisted the latest status: `V201FirmwareUpdateStore` tracked only the in-flight `requestId`. - `V201FirmwareUpdateStore` now retains the latest reported `V201FirmwareStatusReport { status, requestId }` (default `{ Idle, None }`), recorded at the single emit choke point (`send_v201_firmware_status`) so the full lifecycle — interim and terminal, happy path and injected failures — is captured. It is deliberately *not* cleared with the in-flight slot, so a settled `Installed` (or a terminal failure) stays reportable. - `v201_trigger_message_status` reclassifies `FirmwareStatusNotification` as `Accepted`; the existing enqueue path then drains it to a new dispatch arm. - `ChargePoint::trigger_v201_firmware_status_notification` re-reports the latest status as one `FirmwareStatusNotification` — a pure snapshot that starts no update and leaves the in-flight store untouched. Runs on the command-consumer task (off the inbound-CALL path), so the `TriggerMessage` CALLRESULT flushes first and the receive loop never re-enters. - A never-ran station reports `Idle` with `requestId` omitted (the 2.0.1 schema omits `requestId` for a status not tied to a request); a new `v201_firmware_status_report` builder takes an `Option<i32>` for that, with the async-progress builder delegating to it. The sibling triggers stay `NotImplemented` and are tracked as follow-ups: LogStatusNotification (#584), PublishFirmwareStatusNotification (#585); the certificate-signing triggers are a separate, heavier slice. Ports the Charging Station behavior pinned by the already-ported types (`ocpp/v201/enums.py` FirmwareStatusEnumType / MessageTriggerEnumType, `ocpp/v201/call.py` FirmwareStatusNotification / TriggerMessage). Tests: store (latest-status tracking, retention past the in-flight clear, extreme requestIds); pure builder (`Idle` omits `requestId`, `Some` carries it, schema validity, wrapper equivalence); policy (`FirmwareStatusNotification` is `Accepted`, totals updated); and over-the-wire via a capturing mock CSMS (idle re-report, latest-status re-report, and the real rollout recording its terminal). The existing NotImplemented-trigger integration test now uses `LogStatusNotification`. `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test --workspace` all green. Closes #583 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EPMgZ2Vem4gFw75rQv9i1f
This was referenced Sep 28, 2026
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.
Summary
Wires the OCPP 2.0.1 CP simulator to honor
TriggerMessage(requestedMessage = FirmwareStatusNotification)(M7): it now re-reports the station's latest firmware status as oneFirmwareStatusNotification, instead of declining the trigger asNotImplemented. This is the direct v201 twin of the 1.6J behavior the CP already has.Closes #583Real use case
A field operator has just pushed new firmware to a fleet of chargers via
UpdateFirmware. The rollout progress (Downloading → Downloaded → Installing → Installed) is reported asynchronously, but progress notifications can be missed — a flaky link drops a frame, or the back office restarts mid-rollout. To reconcile state, the CSMS asks a specific station "where are you now?" withTriggerMessage(FirmwareStatusNotification), and the station answers with its current firmware status. Before this change the v201 simulator declined that request (NotImplemented), so a back office reconciling a 2.0.1 fleet's firmware state got no answer — even though the simulator was modeling the rollout the whole time. Now it replies with the live status:Installed(with the rollout'srequestId) if it finished, an interim step if still underway, a failure terminal if it failed, orIdleon a station that has never updated.What changed
crates/ocpp-cp:v201_firmware_update.rs—V201FirmwareUpdateStorenow retains the latest reportedV201FirmwareStatusReport { status, requestId }(default{ Idle, None }), alongside the existing in-flightrequestId. It is recorded at the single emit choke point (send_v201_firmware_status), so the whole lifecycle — interim and terminal, happy path and injected failures — is captured, and it is deliberately not cleared when the in-flight slot is (complete/clear): a settledInstalled(or a terminal failure) must stay reportable.v201_command.rs—v201_trigger_message_statusreclassifiesFirmwareStatusNotificationfromNotImplementedtoAccepted; the existing inbound-handler enqueue path then routes it to the new dispatch arm with no other wiring change. A new purev201_firmware_status_report(status, Option<i32>)builder backs the re-report (the async-progressv201_firmware_status_notificationnow delegates to it), so anIdlesnapshot can omitrequestId.lib.rs—ChargePoint::trigger_v201_firmware_status_notificationreads the latest status and emits oneFirmwareStatusNotification.send_v201_firmware_statusrecords the status before sending. Thesend_v201_triggered_messagematch gains theFirmwareStatusNotificationarm.Failure modes / trust boundary. The re-report is a pure snapshot: it starts no update and leaves the in-flight store untouched. It runs on the command-consumer task (off the inbound-CALL path), so the
TriggerMessageCALLRESULT flushes before this CALL and the receive loop never re-enters.requestIdis an opaquei32(never parsed/indexed — no wire value panics), and the builder's output is schema-validated bycall(). A never-ran station reportsIdlewithrequestIdomitted (the 2.0.1 schema omits it for a status not tied to a request), nevernull. The policy classifier and the dispatch match stay in lockstep over an exhaustiveMessageTriggerEnumType, so a future spec-added trigger is a compile error, not a silent drift. 1.6J is untouched.What was ported
mobilityhouse/ocpp is protocol-only, so the ported artefact is the Charging Station behavior; the wire tokens are pinned by the already-ported types:
ocpp/v201/enums.py—FirmwareStatusEnumType,MessageTriggerEnumType.firmware_status_notification.ocpp/v201/call.py—class FirmwareStatusNotification,class TriggerMessage.Test plan
cargo fmt --all --check,cargo clippy --all-targets -- -D warnings,cargo test --workspaceall green locally (820 passing inocpp-cp, +11).New tests:
v201_firmware_update.rs):a_new_store_reports_idle_with_no_request_id,record_reported_tracks_the_latest_status_and_request_id,completing_the_rollout_retains_the_last_reported_status,record_reported_accepts_extreme_request_ids.v201_command.rs):firmware_status_report_omits_request_id_when_none,firmware_status_report_carries_request_id_when_some,built_firmware_status_reports_are_schema_valid; the trigger-policy tests updated (accepted 6 / not-implemented 5).lib.rs):v201_trigger_firmware_status_reports_idle_when_no_update_ran,v201_trigger_firmware_status_reports_the_latest_recorded_status, andv201_firmware_rollout_records_its_terminal_status_for_re_report(drives the real state machine, happy + failure terminal).v201_trigger_unsupported_message_is_not_implemented_and_emits_nothingintegration test now usesLogStatusNotificationas its still-NotImplementedexample.Acceptance criteria
TriggerMessage(FirmwareStatusNotification)is classifiedAcceptedand originates exactly one schema-validFirmwareStatusNotificationcarrying the station's latest firmware status.requestId) is tracked across the full update lifecycle; a never-ran station reportsIdlewithrequestIdomitted.UpdateFirmware → FirmwareStatusNotificationprogress stream or the 1.6J trigger behavior.cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --workspaceall green.Known gaps / notes
NotImplemented, tracked as follow-ups filed alongside this:LogStatusNotification(port(m7): CP simulator 2.0.1 — honor TriggerMessage(LogStatusNotification) by re-reporting the latest log-upload status #584) andPublishFirmwareStatusNotification(port(m7): CP simulator 2.0.1 — honor TriggerMessage(PublishFirmwareStatusNotification) by re-reporting the latest publish-firmware status #585) will re-report their latest status the same way. The three certificate-signing triggers (SignChargingStationCertificate/SignV2GCertificate/SignCombinedCertificate) are a distinct, heavier slice — a trigger there must originate a freshSignCertificateCSR flow, not re-report a status — and are deliberately out of scope here.🤖 Generated with Claude Code
https://claude.ai/code/session_01EPMgZ2Vem4gFw75rQv9i1f
Generated by Claude Code