Context
DataTransfer is the 2.0.1 vendor-specific escape hatch: a vendorId plus optional messageId and free-form data, answered with a DataTransferStatusEnumType and its own optional data. It is bidirectional — either side may originate it.
The message type is already ported (#154 / PR #156: DataTransferRequest / DataTransferResponse, DataTransferStatusEnumType, bundled schema in SchemaValidator::v201()). And the CP already handles the inbound direction on 1.6J — crates/ocpp-cp/src/data_transfer.rs routes inbound DataTransfer CALLs through a vendor registry (@on("DataTransfer")-style), defaulting to UnknownVendorId.
What is still missing is the CP-initiated (outbound) origination path on the 2.0.1 simulator: a driver hook that lets the charging station originate a DataTransfer.req to the CSMS and surface the typed .conf (status + optional data). This is the same outbound-CALL shape used by request_security_event_notification (#563), request_sign_certificate (#547), and the just-landed request_notify_charging_limit / request_cleared_charging_limit (#564 / PR #566) — not a reply to an inbound CALL.
Real use case: a vendor extension (e.g. a proprietary diagnostics ping, a display-asset push, or an OEM battery-health report) that has no standard OCPP message. The station originates it as DataTransfer and acts on the CSMS's Accepted / Rejected / UnknownMessageId / UnknownVendorId verdict.
Python reference
ocpp/v201/call.py — class DataTransfer (vendor_id: str, optional message_id: str, optional data: Any).
ocpp/v201/call_result.py — class DataTransfer (status: DataTransferStatusEnumType, optional status_info, optional data: Any).
ocpp/v201/enums.py — DataTransferStatusEnumType (Accepted / Rejected / UnknownMessageId / UnknownVendorId).
Proposed design
- A pure
v201_data_transfer_request(vendor_id, message_id, data) builder in v201_command, threading its inputs verbatim (data is free-form serde_json::Value, Optional[Any] in the reference), with a schema-validity test against the bundled 2.0.1 FINAL JSON Schema.
- A V201-only driver hook
ChargePoint::request_data_transfer(vendor_id, message_id, data) -> Result<DataTransferResponse, OcppError> that emits the CALL inline via call() (which schema-validates the outgoing request and the incoming .conf against the CP's 2.0.1 validator) and returns the typed response (status + optional data), so the caller can branch on the verdict. A 1.6J station is refused with OcppError::NotSupported (mirrors request_security_event_notification).
- Unlike the ack-only notifications,
DataTransfer.conf is not empty — return the parsed DataTransferResponse rather than Ok(()), so status / data / statusInfo are visible to the caller.
Acceptance criteria
Milestone / labels
M7 — OCPP 2.0.1. Labels: port, m7. Small (one builder + one hook + tests; well under the ~500-LOC target).
Known gaps / notes
Filed by the nightly dev during the 2026-09-08 run while landing the #566 merge and closing #564 — replenishing the M7 CP-initiated backlog. The v201 message type already exists (#154 / #156); this is the missing outbound driver hook.
Context
DataTransferis the 2.0.1 vendor-specific escape hatch: avendorIdplus optionalmessageIdand free-formdata, answered with aDataTransferStatusEnumTypeand its own optionaldata. It is bidirectional — either side may originate it.The message type is already ported (#154 / PR #156:
DataTransferRequest/DataTransferResponse,DataTransferStatusEnumType, bundled schema inSchemaValidator::v201()). And the CP already handles the inbound direction on 1.6J —crates/ocpp-cp/src/data_transfer.rsroutes inboundDataTransferCALLs through a vendor registry (@on("DataTransfer")-style), defaulting toUnknownVendorId.What is still missing is the CP-initiated (outbound) origination path on the 2.0.1 simulator: a driver hook that lets the charging station originate a
DataTransfer.reqto the CSMS and surface the typed.conf(status + optional data). This is the same outbound-CALL shape used byrequest_security_event_notification(#563),request_sign_certificate(#547), and the just-landedrequest_notify_charging_limit/request_cleared_charging_limit(#564 / PR #566) — not a reply to an inbound CALL.Real use case: a vendor extension (e.g. a proprietary diagnostics ping, a display-asset push, or an OEM battery-health report) that has no standard OCPP message. The station originates it as
DataTransferand acts on the CSMS'sAccepted/Rejected/UnknownMessageId/UnknownVendorIdverdict.Python reference
ocpp/v201/call.py—class DataTransfer(vendor_id: str, optionalmessage_id: str, optionaldata: Any).ocpp/v201/call_result.py—class DataTransfer(status: DataTransferStatusEnumType, optionalstatus_info, optionaldata: Any).ocpp/v201/enums.py—DataTransferStatusEnumType(Accepted/Rejected/UnknownMessageId/UnknownVendorId).Proposed design
v201_data_transfer_request(vendor_id, message_id, data)builder inv201_command, threading its inputs verbatim (datais free-formserde_json::Value,Optional[Any]in the reference), with a schema-validity test against the bundled 2.0.1 FINAL JSON Schema.ChargePoint::request_data_transfer(vendor_id, message_id, data) -> Result<DataTransferResponse, OcppError>that emits the CALL inline viacall()(which schema-validates the outgoing request and the incoming.confagainst the CP's 2.0.1 validator) and returns the typed response (status + optional data), so the caller can branch on the verdict. A 1.6J station is refused withOcppError::NotSupported(mirrorsrequest_security_event_notification).DataTransfer.confis not empty — return the parsedDataTransferResponserather thanOk(()), sostatus/data/statusInfoare visible to the caller.Acceptance criteria
DataTransfer.req(vendorIdrequired; optionalmessageId,data), V201-only; 1.6J refused withNotSupported.v201_commandwith schema-validity tests: with and without optionalmessageId/data;dataround-trips arbitrary JSON (object / array / string / number) without loss.messageId/dataomitted are absent on the wire (notnull); requiredvendorIdalways present..confis parsed and the typedDataTransferResponse(status + optionaldata/statusInfo) is surfaced without panic across all fourDataTransferStatusEnumTypevalues; transport / timeout / CALLERROR propagate asOcppError.datais threaded verbatim, never parsed/executed; an oversized/hostile payload surfaces as anErrfrom outbound schema validation, never a panic (test).cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --workspaceall green.Milestone / labels
M7 — OCPP 2.0.1. Labels:
port,m7. Small (one builder + one hook + tests; well under the ~500-LOC target).Known gaps / notes
DataTransferrouting (the CP answering a CSMS-originatedDataTransfer, analogous to the 1.6Jdata_transfer.rsregistry) is a separate seam — out of scope here; this issue is the origination side only.Filed by the nightly dev during the 2026-09-08 run while landing the #566 merge and closing #564 — replenishing the M7 CP-initiated backlog. The v201 message type already exists (#154 / #156); this is the missing outbound driver hook.