Skip to content

feat(ocpp-cp): honor v201 TriggerMessage(FirmwareStatusNotification) by re-reporting the latest firmware status (M7) - #586

Merged
duyhuynh-vn merged 1 commit into
mainfrom
claude/inspiring-ramanujan-oda6je
Sep 28, 2026
Merged

duyhuynh-vn merged 1 commit into
mainfrom
claude/inspiring-ramanujan-oda6je

Conversation

@duyhuynh-vn

Copy link
Copy Markdown
Collaborator

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 one FirmwareStatusNotification, instead of declining the trigger as NotImplemented. This is the direct v201 twin of the 1.6J behavior the CP already has.

Closes #583

Real 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?" with TriggerMessage(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's requestId) if it finished, an interim step if still underway, a failure terminal if it failed, or Idle on a station that has never updated.

What changed

crates/ocpp-cp:

  • v201_firmware_update.rs — V201FirmwareUpdateStore now retains the latest reported V201FirmwareStatusReport { status, requestId } (default { Idle, None }), alongside the existing in-flight requestId. 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 settled Installed (or a terminal failure) must stay reportable.
  • v201_command.rs — v201_trigger_message_status reclassifies FirmwareStatusNotification from NotImplemented to Accepted; the existing inbound-handler enqueue path then routes it to the new dispatch arm with no other wiring change. A new pure v201_firmware_status_report(status, Option<i32>) builder backs the re-report (the async-progress v201_firmware_status_notification now delegates to it), so an Idle snapshot can omit requestId.
  • lib.rs — ChargePoint::trigger_v201_firmware_status_notification reads the latest status and emits one FirmwareStatusNotification. send_v201_firmware_status records the status before sending. The send_v201_triggered_message match gains the FirmwareStatusNotification arm.

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 TriggerMessage CALLRESULT flushes before this CALL and the receive loop never re-enters. requestId is an opaque i32 (never parsed/indexed — no wire value panics), and the builder's output is schema-validated by call(). A never-ran station reports Idle with requestId omitted (the 2.0.1 schema omits it for a status not tied to a request), never null. The policy classifier and the dispatch match stay in lockstep over an exhaustive MessageTriggerEnumType, 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 --workspace all green locally (820 passing in ocpp-cp, +11).

New tests:

  • Store (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.
  • Pure builder (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).
  • Over-the-wire via a capturing mock CSMS (lib.rs): v201_trigger_firmware_status_reports_idle_when_no_update_ran, v201_trigger_firmware_status_reports_the_latest_recorded_status, and v201_firmware_rollout_records_its_terminal_status_for_re_report (drives the real state machine, happy + failure terminal).
  • The existing v201_trigger_unsupported_message_is_not_implemented_and_emits_nothing integration test now uses LogStatusNotification as its still-NotImplemented example.

Acceptance criteria

  • TriggerMessage(FirmwareStatusNotification) is classified Accepted and originates exactly one schema-valid FirmwareStatusNotification carrying the station's latest firmware status.
  • The latest status (+ correlating requestId) is tracked across the full update lifecycle; a never-ran station reports Idle with requestId omitted.
  • The re-report is a pure snapshot — it starts no update and leaves the in-flight store unchanged.
  • No regression to the async UpdateFirmware → FirmwareStatusNotification progress stream or the 1.6J trigger behavior.
  • cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test --workspace all green.

Known gaps / notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01EPMgZ2Vem4gFw75rQv9i1f


Generated by Claude Code

…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
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.

2 participants