feat(ocpp-cp): page v201 NotifyReport / NotifyMonitoringReport across tbc pages (M7) - #575
Merged
Merged
Conversation
… tbc pages (M7) The OCPP 2.0.1 device-model report streams (`GetBaseReport`/`GetReport` → `NotifyReport`, `GetMonitoringReport` → `NotifyMonitoringReport`) emitted a single page regardless of inventory size, hard-coding `seqNo: 0` / `tbc: None`. A large `FullInventory` was silently capped at one frame — a conformance and scaling gap the two emitters explicitly deferred as a "later slice". Close that slice by paging both streams, mirroring the existing `NotifyCustomerInformation` / `ReportChargingProfiles` / `NotifyDisplayMessages` pager idiom (pure page-splitter + thin command-consumer emitter loop): - Add `v201_notify_report_pages` and `v201_notify_monitoring_report_pages` pure builders in `v201_command`, sharing a `v201_report_pages` core that chunks the snapshot into pages of at most `V201_REPORT_ENTRIES_PER_PAGE` (25) entries, numbers them with a monotonic `seqNo` from 0, and flags `tbc: Some(true)` on every page but the last. All pages echo the triggering `requestId` and share one `generatedAt` (the snapshot instant, passed in so the builder stays pure). - An empty report still yields exactly one final page (`seqNo` 0, `tbc` omitted) with the array omitted — the report arrays are `minItems: 1` when present, so an empty page omits `reportData` / `monitor` rather than sending `[]`. - `seqNo` is `i32` on the wire; an `i32::try_from(..).unwrap_or(i32::MAX)` guard means a pathologically long report can never panic on the cast. - Rewire `send_v201_notify_report` / `send_v201_notify_monitoring_report` to emit each page in order via `self.call(..)`, preserving `requestId` correlation and the command-consumer ordering guarantee (pages sent after the triggering CALLRESULT is flushed). Page size (25) sits above the seeded standard profile (~10 entries), so the default station still reports in a single page. Tests: pure-pager unit tests (single / multi-page tbc + monotonic seqNo / empty-one-page / schema validity across shapes) for both streams, plus an over-the-wire integration test that grows the device model past one page and asserts the ordered multi-page `NotifyReport` stream via a capturing mock CSMS. Ports the paging behavior of `ocpp.v201.call.NotifyReport` / `NotifyMonitoringReport` (`tbc`, `seq_no`, `request_id`, `generated_at`). Closes #574. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiC3yVXvLLuCQMfknqv4vb
5 tasks
`cargo doc` runs with `-D warnings` in CI, and `rustdoc::private_intra_doc_links` rejects a public item's docs linking to a private one. The two public report pagers linked to the private `v201_report_pages` core; demote those to plain code spans, matching the fix applied to the NotifyEvent docs previously. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiC3yVXvLLuCQMfknqv4vb
6 tasks
duyhuynh-vn
enabled auto-merge (squash)
September 12, 2026 10:43
Resolve a conflict in crates/ocpp-cp/src/lib.rs test module: #576's SetVariables auto-trip tests and this PR's NotifyReport tbc-paging tests were both appended at the same anchor, and git interleaved them on an identical ChargePoint::new(..) block shared by both tests. Reconstructed each test intact and de-interleaved; both now run and pass. cargo fmt --check, clippy --all-targets -D warnings, and test --workspace all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PeYP9bfNgyvmXsD7aLqFpG
Collaborator
Author
|
🌙 Nightly dev — 2026-09-12: resolved the merge conflict against This PR had drifted to Resolution (merge commit
Verified locally on the merge result:
Generated by Claude Code |
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
Chunk the OCPP 2.0.1 device-model report streams into multiple correlated pages with
toBeContinued(tbc) paging, so a large report is streamed faithfully instead of being silently capped at one WebSocket frame. Advances M7 — OCPP 2.0.1.Closes #574Real use case
A back office asks a charging station for its full device model —
GetBaseReport(FullInventory)— to reconcile firmware/config before a maintenance window. A real station's device model is dozens to hundreds of variables, far more than fits in one practical WebSocket message. OCPP 2.0.1 defines paging precisely for this: the station streams severalNotifyReportCALLs, each carrying a monotonicseqNoandtbc: trueuntil the final page, all correlated byrequestId. Until now the simulator emitted a single page regardless of size, so a large inventory (or a largeGetMonitoringReportmonitor set) was silently truncated — the operator would see only the first frame's worth of variables and reconcile against an incomplete picture. This makes the station report the whole model, in order, across as many pages as it takes.What changed
crates/ocpp-cponly.v201_command— two pure page-splitters,v201_notify_report_pagesandv201_notify_monitoring_report_pages, sharing av201_report_pagescore. Each chunks the snapshot into pages of at mostV201_REPORT_ENTRIES_PER_PAGE(25) entries, numbers them with a monotonicseqNofrom 0, and flagstbc: Some(true)on every page but the last. Every page echoes the triggeringrequestIdand shares onegeneratedAt(the snapshot instant, passed in so the builder stays pure and clock-free).seqNo0,tbcomitted), with the array omitted rather than sent as[]: thereportData/monitorarrays areminItems: 1when present. No zero-page emission.seqNoisi32on the wire; ani32::try_from(..).unwrap_or(i32::MAX)guard means a pathologically long report can never panic on the cast. Report contents are the station's own computed snapshot (not attacker-supplied); an oversized individual entry still surfaces as an outbound-schemaErrfromcall(), never a panic or silent truncation — the same guarantee as the single-page path.lib.rs— rewiredsend_v201_notify_report/send_v201_notify_monitoring_reportto emit each page in order viaself.call(..), preservingrequestIdcorrelation and the command-consumer ordering guarantee (pages sent only after the triggering CALLRESULT is flushed, off the inbound-CALL path). Page size (25) sits comfortably above the seeded standard profile (~10 entries), so the default station still reports in a single page — the existingGetBaseReport(FullInventory)"exactly one page" test is unchanged.What was ported
mobilityhouse/ocpp is protocol-only, so the paging behavior is the Charging Station's, not a Python module; the wire types being paged are already ported. Faithful to the paging contract of
ocpp.v201.call.NotifyReport/NotifyMonitoringReport(tbc,seq_no,request_id,generated_at) andocpp/v201/datatypes.pyReportDataType/MonitoringDataType. Mirrors the in-repov201_notify_customer_information_pages/v201_report_charging_profiles_pagesidiom.Test plan
cargo fmt --all --check✅cargo clippy --all-targets -- -D warnings✅cargo test --workspace✅ (all green)New tests:
v201_command), both streams: small report → one unpaged page (seqNo0,tbcomitted); large report (2*page+1/page+1entries) → correct page count,tbc: Some(true)on all but the last, monotonicseqNo,requestIdechoed, one sharedgeneratedAt, entries preserved in order across the boundary; empty → exactly one final page with the array omitted; every built page (incl. the empty page) is schema-valid.lib, capturing mock CSMS): grow the device model past one page, stream a realFullInventoryreport, and assert the ordered multi-pageNotifyReportstream on the wire (per-pageseqNo/tbc/requestId).Acceptance criteria
tbc: trueon every page but the last, monotonicseqNofrom 0, correlated byrequestId, in order, off the command-consumer task.tbc/seqNounit tests, mirroringv201_notify_customer_information_pages.seqNocast cannot panic; oversized entries surface as an outbound-schemaErr, never a panic/truncation.cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --workspaceall green.Known gaps / notes
NotifyEvent(the thirdtbc-capable stream, single-page today per port(m7): CP simulator 2.0.1 — emit NotifyEvent when a CSMS-installed variable monitor trips #545/feat(ocpp-cp): emit v201 NotifyEvent when a variable monitor trips (M7) #570) is intentionally out of scope — it trips on a different trigger; a separate follow-up if a per-trip event set ever outgrows a frame.🤖 Generated with Claude Code
https://claude.ai/code/session_01CiC3yVXvLLuCQMfknqv4vb
Generated by Claude Code