Skip to main content

Crate ocpp_types

Crate ocpp_types 

Source
Expand description

Strongly typed OCPP message types for 1.6J, 2.0.1, and 2.1.

ocpp-types provides the request/response payload types for the Open Charge Point Protocol, generated from the official JSON schemas. It is no_std and allocation-free: field sizes are bounded at the type level with heapless collections, sized to the limits stated in each version’s specification.

Each protocol version lives in its own module – v16, v201, v21 – since the same message name can differ in shape across versions.

§Fields with no spec-given bound

A handful of fields (free-text strings, a few arrays) have no maxLength/maxItems in the spec, so there’s no size to give a heapless collection without guessing one. These become a const generic the caller picks (with a default, so most code never needs to think about it):

use ocpp_types::v16::DataTransferResponse;
use ocpp_types::v16::common::DataTransferResponseStatus;

// Uses the default capacity (1024):
let response: DataTransferResponse = DataTransferResponse {
    data: Some(heapless::String::try_from("vendor payload").unwrap()),
    status: DataTransferResponseStatus::Accepted,
};

// Or pick a smaller one explicitly:
let response: DataTransferResponse<64> = DataTransferResponse {
    data: Some(heapless::String::try_from("vendor payload").unwrap()),
    status: DataTransferResponseStatus::Accepted,
};

With the alloc feature enabled, these fields become plain alloc::string::String/alloc::vec::Vec<T> instead, and the const generic disappears entirely – useful on targets with a real allocator (a CSMS backend, a simulator) that would rather not pick a bound at all.

§Timestamps

Every version types its dateTime fields as {"type": "string", "format": "date-time"} without a maxLength, so they would fall under the rule above and reserve 1024 bytes each. They are OcppTimestamp instead: 16 bytes, no const generic, no allocator, and comparable – which a string is not.

use ocpp_types::{OcppTimestamp, v16::HeartbeatResponse};

let response = HeartbeatResponse {
    current_time: OcppTimestamp::parse_rfc3339("2024-01-01T00:00:00Z").unwrap(),
};

assert_eq!(response.current_time.unix_seconds(), 1_704_067_200);

Enable the chrono feature for From/Into conversions with chrono::DateTime. That is interop only – chrono is never on the wire path, since its own serde support formats through an allocating to_rfc3339, which the no-alloc build cannot use.

§Sizing for allocation-free targets

With default-features = false every field is stored inline at its declared capacity, so a message’s size_of is the sum of what it could hold, not what it does. Most of the protocol is small under that rule – the median message is a few hundred bytes and around 85% are under 4 KB – but a few families reserve far more, and those need their capacities named rather than defaulted.

Every capacity below is a const generic with a default, so nothing has to be specified to compile; specifying them is how a station trades unused headroom for stack space.

If your station doesSetBecause the default is
Smart chargingchargingSchedulePeriod, chargingProfile caps8 each, and they nest three deep
V2X / bidirectionalv2xFreqWattCurve, v2xSignalWattCurve8 points each, on every schedule period
ISO 15118-20 pricingpriceRuleStacks, priceLevelScheduleEntries, salesTariffEntry8 each; set to 0 if unused
Tariffs (2.1)energyPrices, timePrices, fixedPrices8 each, per tariff kind
Plug and Chargecertificate, certificateChain, csr, signingCertificate1024 – too small for a real PEM chain
Signed meteringsignedMeterData1024; the spec allows 32768
Local auth listslocalAuthorizationList8 entries
MeteringmeterValue, sampledValue8 each, and they multiply

Two of these are worth calling out for opposite reasons. The Plug and Charge fields are the only ones whose default is deliberately too small: a PEM chain will not fit in 1024 bytes, so a deployment that uses certificates must raise them and will discover this immediately in testing. Erring the other way would have cost every deployment that never sees a certificate. Conversely, capacities you set to 0 cost nothing at all, which is the cheapest way to exclude a feature you do not implement.

As a worked example, ReportChargingProfilesRequest defaults to 530 KB. A station that advertises PeriodsPerSchedule = 8, reports one profile at a time, and implements neither V2X nor ISO 15118-20 pricing compiles the same message at 18 KB by naming those capacities.

The specification expects this: SmartChargingCtrlr.PeriodsPerSchedule is a required 2.x variable and 1.6 has ChargingScheduleMaxPeriods, so every station already declares its own limits. Compiling in the capacity you advertise is conformant; reserving the protocol ceiling you will never accept is merely large.

§Checking a payload against the spec

Most spec limits are in the types, so a violation cannot be built: a property bounded at maxLength: 20 is a heapless::String<20>. What is left over is what the validate feature covers — the bounds too large to store inline (see the sizing table above: those fields are a growable String/Vec under alloc, and a caller-chosen capacity without it), plus every minItems, minimum, maximum and multipleOf in the schemas, which no collection type can express at all.

use ocpp_types::v21::CancelReservationRequest;
use ocpp_types::validate::{Validate, ValidationErrorKind};

let request: CancelReservationRequest = CancelReservationRequest {
    custom_data: None,
    reservation_id: -1, // the schema states `minimum: 0`
};

let error = request.validate().unwrap_err();
assert_eq!(
    error.kind(),
    ValidationErrorKind::BelowMinimum { value: -1.0, min: 0.0 },
);

This matters most on the sending side, and most of all for a CSMS: an over-long field that the type accepted comes back from the peer as a CALLERROR with no indication of which field caused it, while validate::ValidationError names the path to it. Nothing calls it for you — see the validate module.

§Fields the spec leaves untyped

2.0.1 and 2.1’s DataTransfer carries a data field the specification deliberately gives no type at all – “open to implementation”, agreed between the two parties. There’s no single Rust type for arbitrary JSON without an allocator, so the payload type is the caller’s to pick, as a type parameter defaulting to () (i.e. “this deployment sends no data”):

use ocpp_types::v201::DataTransferRequest;

// Whatever this vendor agreed on; add `serde::Serialize`/`Deserialize`
// to send it on the wire.
#[derive(Debug, Clone, PartialEq)]
struct VendorPayload {
    session_id: u32,
}

let request: DataTransferRequest<VendorPayload> = DataTransferRequest {
    custom_data: None,
    data: Some(VendorPayload { session_id: 42 }),
    message_id: None,
    vendor_id: heapless::String::try_from("com.example").unwrap(),
};

// Or, sending no vendor payload at all, the default:
let plain: DataTransferRequest = DataTransferRequest {
    custom_data: None,
    data: None,
    message_id: None,
    vendor_id: heapless::String::try_from("com.example").unwrap(),
};

1.6J’s DataTransfer.data is a plain string in that version’s schema, so it stays Option<heapless::String<N>> and needs no parameter.

§Example

use ocpp_types::Action;
use ocpp_types::v16::{AuthorizeRequest, IdTag};

let request = AuthorizeRequest {
    id_tag: IdTag::try_from("ABC123").unwrap(),
};

assert_eq!(AuthorizeRequest::ACTION, "Authorize");

§Serialization

With the serde feature enabled, every message implements serde::Serialize/serde::Deserialize, and Action gains zero-allocation JSON helpers backed by serde-json-core – the caller owns the buffer, nothing is heap-allocated:

use ocpp_types::Action;

let mut buf = [0u8; 256];
let json: &str = request.to_json_str(&mut buf)?;
let parsed = AuthorizeRequest::from_json_str(json)?;

§RPC errors

Each version also exposes an RpcErrorCode enum covering the CALLERROR codes defined by that version’s OCPP-J specification (e.g. v16::RpcErrorCode), implementing core::error::Error.

§WebSocket envelopes

With serde, Call/CallResult/CallError model the OCPP-J array-based envelope every message travels in ([2, messageId, action, payload], etc.) – generic over the payload type, so no per-version duplication is needed. CallResultError/SendMessage cover 2.1’s additional CALLRESULTERROR/SEND message types (the shapes work for any version; whether a given deployment actually uses them is a protocol-level concern, not something the types enforce). See examples/envelope.rs.

Modules§

v16
v21
v201
validate
Checking a payload against the parts of the OCPP specification the types themselves cannot carry.

Structs§

Call
An OCPP-J CALL: [2, "<MessageId>", "<Action>", {Payload}]. T’s action name is written on serialize and validated (not merely assumed) on deserialize, so a Call<T> you get back always really is the T you asked for.
CallError
CallResult
An OCPP-J CALLRESULT: [3, "<MessageId>", {Payload}]. Unlike Call, there’s no action name on the wire – the recipient already knows which action a result is for by correlating message_id against the Call it sent, which is exactly what choosing T here represents.
CallResultError
EmptyPayload
Placeholder for OCPP-J’s errorDetails, which the specification deliberately leaves undefined in shape (“this JSON object describes error details in an undefined way”) and recommends leaving empty in practice. Serializes as {}; deserializes by accepting and discarding whatever object is present, so it round-trips regardless of what a real implementation happens to send. Use a custom type via CallError/CallResultError’s D parameter for typed details.
MessageId
A unique identifier correlating a Call/SendMessage with its CallResult/CallError/CallResultError. Identical across 1.6J, 2.0.1, and 2.1: a string of at most 36 characters, to allow for UUIDs/GUIDs (per the OCPP-J specification).
NoCustomData
Stands in for a customData object this deployment doesn’t read.
OcppDate
A local calendar date, YYYY-MM-DD.
OcppTimeOfDay
A local time of day, HH:MM.
OcppTimestamp
A dateTime value from any OCPP version.
SendMessage
An OCPP-J SEND (OCPP 2.1 only): [6, "<MessageId>", "<Action>", {Payload}]. Structurally identical to Call, but the receiver SHALL NOT respond to it – used for fire-and-forget messages like frequent periodic monitoring values.

Enums§

TimestampError
Why a string is not an OCPP dateTime.

Constants§

MAX_RFC3339_LEN
The widest string OcppTimestamp::to_rfc3339 can produce: -262143-01-01T00:00:00.123456789+01:00. Callers sizing their own buffer should use this.

Traits§

Action