Skip to content

feat(ocpp-cp): honor v201 TriggerMessage(PublishFirmwareStatusNotification) by re-reporting the latest publish-firmware status (M7) - #588

Open
duyhuynh-vn wants to merge 1 commit into
mainfrom
claude/inspiring-ramanujan-rqv9kq
Open

duyhuynh-vn wants to merge 1 commit into
mainfrom
claude/inspiring-ramanujan-rqv9kq

Conversation

@duyhuynh-vn

Copy link
Copy Markdown
Collaborator

Summary

Honors TriggerMessage(requestedMessage = PublishFirmwareStatusNotification) on the v201 CP simulator by re-reporting the station's latest publish-firmware status — the publish-to-local-cache twin of the just-landed TriggerMessage(FirmwareStatusNotification) re-report (#586). Advances M7 (OCPP 2.0.1).

Closes #585

Real use case

A site host acts as a Local Controller: the CSMS sends PublishFirmware so the controller downloads a firmware image once and caches it on the LAN for the chargers behind it, then watches the async PublishFirmwareStatusNotification stream (Idle → DownloadScheduled → Downloading → Downloaded → Published). If the back office restarts or loses that progress CALL, an operator needs to re-confirm where the publish stands — has the image finished caching, and at which LAN URIs? They send TriggerMessage(PublishFirmwareStatusNotification) and expect the controller to answer with its current publish status (e.g. Published with the cached-image location URIs), without kicking off a second download. Before this change the simulator classified that trigger NotImplemented, so the controller couldn't answer even though it models the publish flow.

What changed

  • crates/ocpp-cp/src/v201_publish_firmware.rs — V201PublishFirmwareStore now retains a single latest V201PublishFirmwareStatusReport { status, location, request_id } alongside its in-flight set. record_reported() / last_reported() added; defaults to Idle / no requestId / no location. The snapshot is deliberately not cleared when an in-flight id settles, so a finished Published (with its cached-image location) stays reportable. With independent per-id streams, "latest" is the most recent emit across all of them — the station's current publish status.
  • crates/ocpp-cp/src/v201_command.rs — new v201_publish_firmware_status_report(status, location, request_id: Option<i32>) builds the re-report (Idle omits requestId and location, absent-not-null per the schema's skip_serializing_if); the async-progress v201_publish_firmware_status_notification() becomes a Some(request_id) wrapper over it. v201_trigger_message_status reclassifies PublishFirmwareStatusNotification from NotImplemented → Accepted.
  • crates/ocpp-cp/src/lib.rs — send_v201_publish_firmware_status records the status at its single emit choke point before the send (so the station's notion advances even if the CALL fails). send_v201_triggered_message dispatches to the new pure-snapshot trigger_v201_publish_firmware_status_notification(), which starts no publish and leaves the in-flight set untouched. Runs on the command-consumer task, off the inbound-CALL path.

Failure modes / trust boundary: the trigger and its requestId are CSMS-supplied but stored opaquely as i32 (never parsed/indexed — extremes can't panic); location is simulator-supplied, never attacker input. Never-published → Idle (no requestId/location). A send failure is logged best-effort, never propagated. 1.6J is unaffected.

What was ported

  • ocpp/v201/call.py — class PublishFirmwareStatusNotification, class TriggerMessage.
  • ocpp/v201/enums.py — PublishFirmwareStatusEnumType, MessageTriggerEnumType.publish_firmware_status_notification.

Test plan

  • cargo fmt --all --check ✅
  • cargo clippy --workspace --all-targets -- -D warnings ✅
  • cargo test --workspace ✅ (836 ocpp-cp lib tests + all integration suites, 0 failures)

New tests:

  • Store units — new store reports Idle/none/none; record_reported tracks latest (incl. Published retaining its location); terminal survives the in-flight clear; extreme requestIds don't panic.
  • Builder + schema — ..._report omits requestId/location when None; carries both when Some; wrapper equivalence; schema-valid across status × requestId (± location) combinations.
  • Trigger classification — PublishFirmwareStatusNotification now in the Accepted set; counts updated (7 accepted / 4 not-implemented); totality preserved.
  • End-to-end trigger — Idle when no publish ran; latest-recorded Published snapshot re-reports status + requestId + location and opens no stream; the real publish stream records its Published terminal (with locations) for re-report.

Acceptance criteria

  • TriggerMessage(PublishFirmwareStatusNotification) is Accepted and originates one schema-valid PublishFirmwareStatusNotification with the latest status.
  • Latest status tracked across the publish lifecycle; never-published reports Idle.
  • Pure snapshot — no publish started; no regression to the async PublishFirmware flow or 1.6J.
  • 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_01QivXv2JfX9f8ZjijhLRvFD


Generated by Claude Code

…ation) by re-reporting the latest publish-firmware status (M7)

Closes #585.

The publish-to-local-cache twin of the TriggerMessage(FirmwareStatusNotification)
re-report (#586): a CSMS asking TriggerMessage(requestedMessage =
PublishFirmwareStatusNotification) now gets the station's latest publish status
re-reported as one PublishFirmwareStatusNotification, without re-running a
publish. Previously the v201 TriggerMessage handler classified it NotImplemented
even though the simulator models PublishFirmware.

- V201PublishFirmwareStore now retains a single latest V201PublishFirmwareStatusReport
  (status + optional requestId + optional location) alongside its in-flight set,
  recorded at the send_v201_publish_firmware_status choke point (before the send,
  so the station's notion advances even if the CALL fails) and retained past the
  in-flight marker's clear. Defaults to Idle / no requestId / no location.
- v201_publish_firmware_status_report() builds the re-report with an optional
  requestId (Idle omits it); the async-progress v201_publish_firmware_status_notification()
  becomes a Some(request_id) wrapper over it.
- v201_trigger_message_status reclassifies PublishFirmwareStatusNotification from
  NotImplemented to Accepted; send_v201_triggered_message dispatches to the new
  pure-snapshot trigger_v201_publish_firmware_status_notification().

The terminal Published carries the cached-image location URIs, so the snapshot
retains location to reproduce that terminal shape faithfully; never-published
reports Idle with requestId and location omitted (schema-valid, not null).

Ports ocpp.v201.call.PublishFirmwareStatusNotification / TriggerMessage and
PublishFirmwareStatusEnumType / MessageTriggerEnumType from the mobilityhouse
reference. Store units, builder + schema-validity, trigger-classification, and
end-to-end trigger tests (idle-when-none, latest-recorded snapshot with location
+ no new stream, terminal recorded across the real stream) all added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QivXv2JfX9f8ZjijhLRvFD
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