Skip to content

evlinked/ocpi-rs

Repository files navigation

ocpi-rs

A modern, production-grade OCPI (Open Charge Point Interface) implementation in Rust. Typed models, an async client, and server-side handlers for every OCPI version.

CI Security License: MIT Rust


What is OCPI?

OCPI is the open protocol that lets EV charging networks roam: a Charge Point Operator (CPO) and an e-Mobility Service Provider (eMSP) exchange Locations, Sessions, CDRs, Tariffs, Tokens, and remote Commands over a versioned REST/JSON API with a token-based credentials handshake. ocpi-rs aims to implement the full standard, all versions, as a reusable library/SDK.

Why this project

  • Safety: memory-safe, no unsafe in the type layer (#![forbid(unsafe_code)]).
  • Correctness: types follow the spec; the unsupported case is rejected with an explicit OCPI status_code, never silently dropped.
  • Reusability: a clean SDK (types + client + server traits) you can embed in a CPO or eMSP backend.
  • Portability: small static binaries, rustls by default (no system OpenSSL).

Scope & Non-Goals

In scope

  • Typed models for every OCPI module across versions 2.0, 2.1.1, 2.2, 2.2.1, 2.3.0 (and a forward-scaffold for 3.0).
  • An async client (sender role): version negotiation, credentials handshake, typed module senders.
  • Server handler traits (receiver role) with an optional axum integration.
  • A small CLI for inspecting and validating OCPI parties.

Out of scope (for this repo)

  • OCPP (charger ↔ CSMS) — see the sibling project EVLinked/ocpp-rs.
  • Billing, pricing engines, end-user apps.
  • A hosted, deployable CPO/eMSP service (this is a library you build one with).

Workspace layout

Crate Role
ocpi-types Wire types: response envelope, status codes, common data types, version negotiation. Version-namespaced module models.
ocpi-client Async HTTP client for the sender role (reqwest, rustls).
ocpi-server Receiver-side handler traits + optional axum routers.
ocpi-cli ocpi command-line tool: list versions, validate envelopes.

Quickstart

# Build everything
cargo build --workspace

# List the versions a remote OCPI party supports
cargo run -p ocpi-cli -- versions https://host/ocpi/cpo/ --token <TOKEN>

# Validate a JSON file parses as an OCPI response envelope
cargo run -p ocpi-cli -- validate ./response.json

Roadmap & Milestones

Each milestone maps to a GitHub milestone and a release. OCPI 2.2.1 is the primary production target; older and newer versions follow.

  • M0 — Bootstrap: CI, security, governance, docs, vendored specs
  • M1 — Core foundation: envelope, status codes, transport headers, pagination, common types — v0.1.0
  • M2 — Versions + Credentials/Registration handshake (2.2.1) — v0.2.0
  • M3 — Locations (2.2.1) — v0.3.0
  • M4 — Sessions + CDRs (2.2.1) — v0.4.0
  • M5 — Tariffs + Tokens (2.2.1) — v0.5.0
  • M6 — Commands + ChargingProfiles + HubClientInfo → OCPI 2.2.1 complete ✅ — v1.0.0
  • M7 — OCPI 2.1.1 (+ 2.2 / 2.0 back-coverage) — v1.1.0
  • M8 — OCPI 2.3.0 (Payments, terminals, new fields) — v1.2.0
  • M9 — Conformance, fuzzing, docs site, 3.0 forward-scaffold — v1.3.0+
    • Conformance corpus (2.2.1): spec-example payloads consolidated under conformance/ with a round-trip harness (#225)

OCPI 2.2.1 is feature-complete across ocpi-types, ocpi-client, and ocpi-server (all 10 modules: types + sender methods + receiver handlers/routers). Remaining 2.2.1 work is non-blocking polish — end-to-end smoke tests (#23, #32, #71, #72) and the ChargingProfiles Sender PUT method (#75).

Module × version support matrix

Legend: ☐ planned · ◑ in progress · ☑ done

Module 2.1.1 2.2 2.2.1 2.3.0
Versions
Credentials
Locations
Sessions
CDRs
Tariffs
Tokens
Commands
ChargingProfiles
HubClientInfo
Payments

☑ = types + client sender methods + server receiver handler/router shipped for 2.2.1. The ChargingProfiles Sender interface — the CPO-pushes-ActiveChargingProfile PUT (#75) — is now complete via charging_profiles_sender_router + OcpiClient::put_active_charging_profile.

☑ (2.2 column) = the OCPI 2.2 back-coverage sub-track is complete (#171). Because OCPI 2.2.1 is a patch release over 2.2, v2_2 re-exports the wire-identical 2.2.1 types by default and overrides only the genuine 2.2-vs-2.2.1 wire deltas (per specs/ocpi/2.2.1/version_history.asciidoc). The role-bearing Endpoint/VersionDetails version layer is shared (the role field arrived in 2.2), and ModuleID already covers the full 2.2 set (#149). The CDRs wire-delta overrides now land: v2_2::cdrs defines CdrToken (no country_code/party_id), Cdr (no home_charging_compensation), and CdrLocation (required postal_code, no state) as v2_2-local types; SignedData/SignedValue stay re-exports because the 2.2.1 change to them is a CiString-length relaxation this crate already models as unbounded String (#153). The Commands wire-delta override now lands too: v2_2::commands defines StartSession without connector_id (in 2.2 the Charge Point picks the connector); the START_SESSION_CONNECTOR_REQUIRED capability 2.2.1 added stays a documented re-export (#157). The Locations wire-delta overrides now land too: v2_2::locations defines PowerType (no AC_2_PHASE/AC_2_PHASE_SPLIT) and ConnectorType (no 2.2.1-added CHAOJI/DOMESTIC_MO/GBT_AC/GBT_DC/extended-NEMA values) as v2_2-local enums that reject a 2.2.1-only value rather than coerce it. The composites that embed those enums — Connector/Evse/Location — are now v2_2-local too: structurally identical to their 2.2.1 counterparts but carrying the 2.2 connector standard/power_type, so a 2.2.1-only plug/power value cannot ride a 2.2 connector even inside a full Location (#158 / #167). With CDRs, Commands, and Locations sliced, the 2.2 type-level wire deltas are complete. The first 2.2 client + server wiring now lands on top of these delta types: the CDRs module is transported end-to-end over v2_2::Cdr — the client sender list + receiver getter/push (OcpiClient::{get_cdrs,get_cdr,post_cdr}_2_2) and the server cdrs_2_2_router + Cdrs22Config / Cdrs22Handler (#163). A CDR is a server-owned object named via the Location header on POST /cdrs (§8.2.2), so the 2.2 paths are flat (POST /cdrs, GET /cdrs/{id}, GET /cdrs) — identical to 2.2.1/2.1.1; only the Cdr object shape differs (a CdrToken with no country_code/party_id, a required CdrLocation.postal_code with no state, no home_charging_compensation), so a 2.2 partner's CDR round-trips faithfully instead of being coerced through the 2.2.1 struct. The Commands module is now end-to-end for 2.2 too: OcpiClient::start_session_2_2 sends the 2.2 StartSession (no connector_id), and commands_2_2_router + Commands22Config/Commands22Handler expose the full receiver surface (CANCEL_RESERVATION/RESERVE_NOW/START_SESSION/STOP_SESSION/UNLOCK_CONNECTOR + the async result callback), deserializing START_SESSION into v2_2::StartSession so a Sender-pinned connector cannot be carried into a 2.2 session; the other four commands and the CommandResult callback are wire-identical to 2.2.1 and reuse the existing methods (#165). The Locations client sender getters now land on those composites too — OcpiClient::{get_locations,get_location,get_evse,get_connector}_2_2 deserialize a 2.2 partner's catalogue into the v2_2 Connector/Evse/Location, so a 2.2.1-only plug/power value is rejected on the 2.2 path rather than silently coerced (#167). The Locations server side now completes the module: locations_2_2_router (receiver, owner-qualified {country_code}/{party_id}/{location_id}[/{evse_uid}[/{connector_id}]] GET/PUT/PATCH at all three levels), locations_2_2_sender_router (the CPO's own flat GET /locations catalogue, paginated with X-Total-Count/X-Limit/Link, kept a separate router so its flat connector path never collides with the receiver's owner path), and the in-memory Locations22Config store — all deserializing into the 2.2 composites, so a 2.2.1-only connector enum pushed onto a 2.2 receiver path is rejected on deserialize, the server-side mirror of the client getter guard (#170 / #167). With CDRs, Commands, and Locations all wired client+server, the three 2.2-vs-2.2.1 delta modules are complete; the remaining seven modules — Sessions, Tariffs, Tokens, ChargingProfiles, HubClientInfo, Versions, Credentials — are wire-identical to 2.2.1 and reuse it by design: no _2_2 client/server methods are minted for them (aliasing identical-typed calls would only imply a difference that does not exist), so a 2.2 party drives them with the existing 2.2.1 surface unchanged. That reuse is ratified, not merely asserted — a compile-time identity assertion per wire-identical module (crates/ocpi-types/src/v2_2/mod.rs, #173) fails CI the moment one silently forks, a v2_2::Session type-level round-trip proves the bytes agree, and a transport-level end-to-end test drives a v2_2::Tariff through the unqualified 2.2.1 client + tariffs_router (PUTGET→list→DELETE, crates/ocpi-client/tests/m7_tariffs_reuse_2_2.rs). With the three delta modules wired and the seven reuse modules verified, the 2.2 column is ☑ (#171).

M7 status — complete. The 2.2 back-coverage sub-track is complete (the three delta modules wired client+server; the seven wire-identical modules verified as reuse — see the 2.2 column note above, #171). The 2.1.1 track is functionally complete across all modules (below). The 2.0 back-coverage slice is resolved as recognition-only (#182): OCPI 2.0 predates the AsciiDoc spec and the roaming ecosystem this SDK targets and is not vendored upstream, so its definition of done is graceful recognition, not a type surface. Concretely, VersionNumber::V2_0 parses/orders "2.0" (lowest, V2_0 < V2_1_1 < V2_2 < V2_2_1 < V2_3_0) and version negotiation degrades a 2.0-only partner to no common version — which the caller maps to an explicit UnsupportedVersion status_code — rather than mis-handling a version the SDK does not speak; the negotiate_disjoint_returns_none test in crates/ocpi-client/src/lib.rs is the regression fence for that path. No v2_0 type module is planned and no 2.0 support-matrix column is added (see specs/ocpi/2.0/README.md for the full contract). With 2.1.1 done, 2.2 done, and 2.0 recognition-only, M7 is complete.

M7 (OCPI 2.1.1) modules. The Versions module is now end-to-end for 2.1.1: the role-less foundation (v2_1_1::Endpoint / v2_1_1::VersionDetails, #86), the client negotiate_version helper (#103), and the server advertising both 2.1.1 and 2.2.1 via VersionsConfig::add_legacy_version so GET /versions/2.1.1 serves a role-less catalogue (#99). The Credentials module is now end-to-end for 2.1.1 over the flat object: client sender methods (OcpiClient::{register,get_credentials,update_credentials}_2_1_1) and the server receiver credentials_2_1_1_router + Credentials2111Config, running the Token A→B→C handshake (#112). The 2.1.1 registration fetch-back (the role-less /versions callback) now lands too: a LegacyVersionFetcher returns the role-less v2_1_1::VersionDetails a faithful 2.1.1 partner emits, and Credentials2111Config::new_with_fetcher GETs the registering party's /versions, negotiates the highest mutual version, and stores its endpoint catalogue (failure → status code 3001) (#115). The Commands module is now end-to-end for 2.1.1: on top of the data types (#93) — CommandType, CommandResponseType, CommandResponse, ReserveNow/StartSession/StopSession/UnlockConnector, faithful to the spec's single-CommandResponseType model (no 2.2 CommandResultType split) — the client sender (OcpiClient::{reserve_now,start_session,stop_session,unlock_connector}_2_1_1 + post_command_result_2_1_1) and the server receiver commands_2_1_1_router + Commands2111Config / Commands2111Handler land (#124). The 2.1.1 flat receiver path (POST /commands/{command}) mirrors 2.2.1 but omits CANCEL_RESERVATION (a 2.2 addition), carries the full Token object in START_SESSION, and reuses the single CommandResponse for both the synchronous ack and the async response_url callback (§13.2.2.1). The Locations module is now end-to-end for 2.1.1: the client sender getters (OcpiClient::{get_locations,get_location,get_evse,get_connector}_2_1_1, #113) and the server receiver locations_2_1_1_router + Locations2111Config (#125), exposing GET/PUT/PATCH at all three object levels. Per OCPI 2.1.1 §2.2 (eMSP Interface) the Locations receiver keeps the {country_code}/{party_id}/{location_id}[/{evse_uid}[/{connector_id}]] path (a client-owned object push) — identical to 2.2.1; only the object shape differs (required type, no country_code/party_id on the object, singular Connector.tariff_id). The 2.1.1 Locations sender side now lands too: locations_2_1_1_sender_router + Locations2111Config::{list,get_by_id,get_evse_by_id,get_connector_by_id} serve a CPO's own catalogue on the flat §2.1 CPO-Interface path (paginated GET /locations with X-Total-Count/X-Limit/Link, plus GET /locations/{location_id}[/{evse_uid}[/{connector_id}]]) — the server-side counterpart to the client get_locations_2_1_1 getters, kept a separate router so its flat connector path never collides with the receiver's owner-qualified path (#142). The Sessions module is now end-to-end for 2.1.1: the client sender list + receiver getters/setters (OcpiClient::{get_sessions,get_session,put_session,patch_session}_2_1_1) and the server sessions_2_1_1_router + Sessions2111Config (#120). Per OCPI 2.1.1 §9.2.2 the Sessions receiver path keeps the {country_code}/{party_id}/{session_id} segments (a client-owned object) — identical to 2.2.1; only the Session object shape differs (auth_id, embedded location, one-word start_datetime, no charging_preferences). The CDRs module is now end-to-end for 2.1.1: the client sender list + receiver getter/push (OcpiClient::{get_cdrs,get_cdr,post_cdr}_2_1_1) and the server cdrs_2_1_1_router + Cdrs2111Config (#120). A CDR is a server-owned object named via the Location header on POST /cdrs (§10.2.2), so the 2.1.1 paths are flat (POST /cdrs, GET /cdrs/{id}, GET /cdrs) — identical to 2.2.1; only the Cdr object shape differs (bare auth_id, embedded location, stop_date_time, a single numeric total_cost, no session_id). The Tariffs module is now end-to-end for 2.1.1: client sender list (OcpiClient::get_tariffs_2_1_1) plus receiver getters/setters (get_tariff_2_1_1/put_tariff_2_1_1/delete_tariff_2_1_1) and the server tariffs_2_1_1_router + Tariffs2111Config (#122) — the 2.1.1 transport paths are identical to 2.2.1 (Sender flat GET /tariffs; Receiver {country_code}/{party_id}/{tariff_id}), only the Tariff object shape differs. The Tokens module has its 2.1.1 server side: the tokens_2_1_1_router + Tokens2111Config expose the receiver (GET/PUT/PATCH /tokens/{country_code}/{party_id}/{token_uid}) and the real-time POST /tokens/{token_uid}/authorize (#123) — per §12.2.2 the receiver path keeps the {country_code}/{party_id}/{token_uid} segments (a client-owned object), identical to 2.2.1; only the payload differs (the Token keys on auth_id with OTHER/RFID only, and AuthorizationInfo omits token/authorization_reference — its LocationReferences keeps the 2.1.1-only connector_ids). The 2.1.1 Tokens client sender now completes the module too: OcpiClient::{get_tokens,put_token,patch_token,authorize_token}_2_1_1 drive the sender list, the receiver upsert/merge-patch, and the real-time authorize (#123). Remaining 2.1.1 module types are tracked in #89#93.

M8 (OCPI 2.3.0) is now kicked off with its foundation module. ocpi_types::v2_3_0 follows the same alias-by-default / override-only-the-deltas convention v2_2 uses: it re-exports the wire-identical 2.2.1 type-set and the shared role-bearing version/endpoint layer, and documents the 2.3.0-over-2.2.1 delta map from specs/ocpi/2.3.0/changelog.asciidoc — the new Payments module, the Locations additions (a Parking object linked to the EVSE with vehicle types, EVSE accepted_emsps, ISO 15118 Plug-and-Charge flags on the Connector, a Location support phone, accessibility info), North-American tax support on Tariffs/CDRs, and the Credentials hub party ID additions. Because 2.3.0 is an additive superset of 2.2.1, each delta lands as a v2_3_0-local override in a follow-up; compile-time type-identity assertions keep the re-exports honest until then, so the support-matrix 2.3.0 column stays ☐ (foundation only, not module-complete) (#174). The first delta — the new Payments module — now lands as the v2_3_0::payments submodule: the Terminal and FinancialAdviceConfirmation objects plus the InvoiceCreator / CaptureStatusCode enums, with ModuleID::Payments ("payments") added to the shared version enum. This is the direct-payment surface a PTP and CPO exchange (a terminal maps Locations/EVSEs to a physical payment terminal; a financial-advice confirmation carries the captured amount + EFT data when the CPO issues the invoice). The Payments server receiver wiring now lands too: the CPO Receiver interface (specs/ocpi/2.3.0/mod_payments.asciidoc §400) is exposed as payments_2_3_0_router + Payments230Config / Payments230HandlerGET/POST payments/terminals and GET/POST payments/financial-advice-confirmations, deserializing into the v2_3_0::payments objects so an over-length CiString(36) terminal/confirmation id is rejected on deserialize before it can reach the store. The PTP-side client sender (the CPO→PTP direction) now completes the module: the read getters (OcpiClient::{get_terminals,get_terminal,get_financial_advice_confirmations,get_financial_advice_confirmation}_2_3_0) plus the four mutating terminal calls (activate_terminal_2_3_0 — the PTP assigns the terminal_iddeactivate_terminal_2_3_0, put_terminal_2_3_0, and patch_terminal_2_3_0 for the Location/EVSE assignment), covering the full §82 Sender interface over the v2_3_0::payments types. With both the CPO Receiver router and the PTP Sender client wired, the 2.3.0/Payments cell is (#176 / #185). The Credentials delta now lands too: the override v2_3_0::credentials::Credentials adds the optional hub_party_id field (CiString(5) — a hub platform's concatenated country-code + party-ID) per specs/ocpi/2.3.0/credentials.asciidoc, while CredentialsRole stays a re-export because the 2.3.0 change is only which roles a hub lists (its reachable parties, as normal roles entries), not the role's wire shape; the Credentials identity assertion is correspondingly dropped since it is now a genuine fork (#179). The 2.3.0 Credentials transport now lands on top of that fork (see the dedicated note below), so the 2.3.0 / Credentials cell is . The Tariffs North-American tax override now lands too: v2_3_0::tariffs forks Tariff to carry the required tax_included flag (TaxIncluded: YES/NO/N/A — whether the listed prices already include tax), tax-aware min_price/max_price as the new PriceLimit (a before_taxes bound plus an optional after_taxes one, replacing the VAT-only Price), and the preauthorize_amount a Payment Terminal Provider should reserve; the wire-identical sub-types stay 2.2.1 re-exports and the Tariff identity assertion is dropped as it forks, so the 2.3.0/Tariffs cell is ◑ (#178). The North-American tax value types now land too as slice 1 of the CDRs/Sessions tax rework: v2_3_0::price defines the reworked Price (before_taxes + an itemised taxes: Vec<TaxAmount>, replacing the VAT-only excl_vat/incl_vat) and the new TaxAmount (name/account_number/percentage/amount) per specs/ocpi/2.3.0/types.asciidoc, so a Canadian GST + QST cost breakdown round-trips faithfully instead of being coerced into a VAT field; those are the load-bearing value types the Cdr/Session cost-field forks are written against (#188). Those forks now land as slices 2 & 3: v2_3_0::cdrs forks Cdr so all six cost fields (total_cost + the five optional total_*_costs) carry the reworked Price, and its tariffs list embeds the 2.3.0 Tariff (with the tax_included flag) rather than the 2.2.1 one; v2_3_0::sessions forks Session so its total_cost carries the reworked Price too. The wire-identical sub-objects (CdrToken, CdrLocation, ChargingPeriod — which carries only metered dimensions, no cost — SignedData, SessionStatus) stay 2.2.1 re-exports, and the Cdr/Session identity assertions are dropped as they fork; a North-American CDR/Session with an itemised GST+QST breakdown round-trips faithfully while a missing required total_cost/before_taxes is rejected on deserialize, so the 2.3.0 / CDRs and 2.3.0 / Sessions matrix cells are (types delta landed; transport wiring is the remaining slice). The Locations Parking/15118 additions follow as their own overrides.

The first Locations delta now lands (slice 1 of #177). v2_3_0::locations defines the new Parking object — a parking space linked to charging, carrying the EU AFIR / National-Access-Point reporting properties (vehicle types, dimensions, direction, reservation/roofing/lighting flags, APDS reference) — plus its VehicleType / ParkingDirection enums, and forks Location to add parking_places (the list of Parking) and help_phone (a Driver support number, CiString(25)). Both new Location fields are skip_serializing_if-guarded, so a 2.3.0 Location that carries no parking data round-trips wire-identically to 2.2.1; the fork keeps a 2.2.1 peer on the exact 2.2.1 shape while a 2.3.0 peer's parking report is parsed and held faithfully. Parking/Location deserialize through the crate's strict enums and CiString length checks, so an unknown VehicleType or an over-length help_phone is rejected on deserialize rather than silently coerced. Slice 2 now completes the Locations type deltas: v2_3_0::locations forks Connector to add capabilities (the ISO 15118 Plug-and-Charge ConnectorCapability flags — ISO_15118_2_PLUG_AND_CHARGE / ISO_15118_20_PLUG_AND_CHARGE) and Evse to add parking (a list of EvseParking linking to a Parking by id, with an optional EvsePosition) and accepted_service_providers; Location now embeds the v2_3_0-local Evse, so both composites drop their reuse_types_stay_aliases_of_2_2_1 identity assertions. All new fields are skip_serializing_if-guarded, so a 2.3.0 object carrying none of them round-trips wire-identically to 2.2.1. The Locations transport now begins landing on those composites from both sides. The client sender getters land first: OcpiClient::{get_locations,get_location,get_evse,get_connector}_2_3_0 deserialize a 2.3.0 partner's catalogue into the v2_3_0 Location/Evse/Connector, so the additive 2.3.0 fields — parking_places/help_phone, Evse.parking/accepted_service_providers, and the ISO 15118 Connector.capabilities — reach the caller instead of being dropped through the 2.2.1 struct, while an unknown VehicleType/ConnectorCapability is still rejected on deserialize rather than coerced (#199). The matching CPO server sender router locations_2_3_0_sender_router + the in-memory Locations230Config store then serve a CPO's own 2.3.0 catalogue on the flat sender path (paginated GET /locations with X-Total-Count/X-Limit/Link, plus GET /locations/{location_id}[/{evse_uid}[/{connector_id}]]), kept a separate router so its flat connector path never collides with a receiver's owner-qualified path — the server-side counterpart to those getters, closing the 2.3.0 Locations sender loop end to end. The 2.3.0 / Locations matrix cell stays — the composite types + the client sender getters + the server sender router are now in; only the server receiver (owner-qualified GET/PUT/PATCH) router remains (#177).

The Payments transport now begins landing (slice of #185, the wiring follow-up to the v2_3_0::payments types). The PTP-Sender client getters land first: OcpiClient::{get_terminals, get_terminal, get_financial_advice_confirmations, get_financial_advice_confirmation}_2_3_0 fetch a PTP's Terminal catalogue and its FinancialAdviceConfirmations over v2_3_0::payments — the two list endpoints paginated over the shared X-Total-Count/X-Limit/Link plumbing, all four carrying the OCPI routing headers a functional module requires. This is the read side a hub uses to map a card tap to a charge point (via a terminal's location_ids/evse_uids) and to pull the financial-advice confirmations that correlate a captured payment to its Session/CDR. The CPO-Receiver router (the write side a CPO exposes) and the PTP-sender mutating calls (activate_terminal_2_3_0 — with its terminal_id-less activation body variant — deactivate_terminal_2_3_0, put_terminal_2_3_0, patch_terminal_2_3_0) now land too, so the full §82 PTP Sender interface is present alongside the receiver and the 2.3.0 / Payments matrix cell is (#185).

The CDRs transport now lands end-to-end for 2.3.0, completing the CDRs half of #203 (its sibling Sessions half is #204). On top of the tax-forked v2_3_0::Cdr (all six cost fields — total_cost + the five optional total_*_costs — reworked onto the itemised-tax 2.3.0 Price, its tariffs list embedding the 2.3.0 Tariff), the client sender + receiver methods (OcpiClient::{get_cdrs, get_cdr, post_cdr}_2_3_0) and the server cdrs_2_3_0_router + Cdrs230Config / Cdrs230Handler now transport a 2.3.0 CDR over HTTP. A CDR is a server-owned object named via the Location header on POST /cdrs, so the paths are flat (POST /cdrs, GET /cdrs/{id}, paginated GET /cdrs with X-Total-Count/X-Limit/Link) — identical to 2.2.1; only the object shape differs, so a North-American CDR's billing total keeps its itemised GST+QST breakdown across the wire instead of collapsing into the VAT-only 2.2.1 field, and a POST body whose total_cost omits the required before_taxes is rejected on deserialize before it reaches the store. The 2.3.0 / CDRs matrix cell moves ◑→☑.

The Tariffs transport now lands end-to-end for 2.3.0, completing the module (#178). On top of the North-American tax fork of v2_3_0::Tariff (the required tax_included flag, tax-aware PriceLimit min_price/max_price, and preauthorize_amount), the client sender methods (OcpiClient::{get_tariffs, get_tariff, put_tariff, delete_tariff}_2_3_0) and the server receiver tariffs_2_3_0_router + Tariffs230Config / Tariffs230Handler now transport a 2.3.0 tariff over HTTP. The transport paths are identical to 2.2.1 (Sender flat GET /tariffs, paginated with X-Total-Count/X-Limit/Link; Receiver {country_code}/{party_id}/{tariff_id} GET/PUT/DELETE) — only the object shape differs, so a Canadian GST+QST tariff pushed through the receiver is stored and re-served with its tax_included stance intact instead of coerced through the VAT-only 2.2.1 shape, and a PUT body missing the required tax_included is rejected on deserialize before it reaches the store. With types + client sender + server receiver all shipped, the 2.3.0 / Tariffs matrix cell moves ◑→☑.

The Sessions transport now lands end-to-end for 2.3.0, completing the module (#203). On top of the tax-forked v2_3_0::Session (its total_cost reworked onto the itemised-tax 2.3.0 Price), the client sender + receiver methods (OcpiClient::{get_sessions, get_session, put_session, patch_session}_2_3_0) and the server sessions_2_3_0_router + Sessions230Config / Sessions230Handler now transport a 2.3.0 session over HTTP. The transport paths are identical to 2.2.1 (Sender flat GET /sessions, paginated with X-Total-Count/X-Limit/Link; Receiver {country_code}/{party_id}/{session_id} GET/PUT/PATCH) — only the object shape differs, so a North-American session's running total keeps its itemised GST+QST breakdown across the wire instead of collapsing into the VAT-only 2.2.1 field, and a total_cost present but missing the required before_taxes is rejected on deserialize before it reaches the store. With the sibling CDRs transport landing alongside it, the 2.3.0 / Sessions matrix cell moves ◑→☑.

The five wire-identical reuse modules now close out the reuse half of the 2.3.0 column (#205) — the direct M8 mirror of the 2.2-column close-out (#171). The 2.3.0 changelog's entire delta set (Payments, the Locations Parking/15118/accepted_emsps additions, the North-American tax rework on Tariffs/CDRs/Sessions, the Credentials hub_party_id, and the "make OCPI extensible" serde policy) touches none of Versions, Tokens, Commands, ChargingProfiles, HubClientInfo — in 2.3.0 those five modules are byte-for-byte the 2.2.1 modules. So, exactly as v2_2 does for the seven wire-identical 2.2 modules, v2_3_0 reuses them rather than minting _2_3_0 client/server methods (aliasing identical-typed calls would only imply a difference that does not exist): a 2.3.0 party drives all five over the existing unqualified 2.2.1 surface unchanged. That reuse is ratified, not merely asserted — the reuse_types_stay_aliases_of_2_2_1 compile-time identity assertions in crates/ocpi-types/src/v2_3_0/mod.rs (now extended with a Versions-layer Endpoint/VersionDetails line) fail CI the moment one silently forks; v2_3_0::Token and v2_3_0::ChargingProfile type-level round-trips prove the bytes agree with 2.2.1; and a transport-level end-to-end test drives a v2_3_0::Token through the unqualified 2.2.1 client + tokens_router (PUTGET list→authorize, crates/ocpi-client/tests/m8_tokens_reuse_2_3_0.rs). Nothing loosens — every strict-enum / CiString-length rejection the 2.2.1 modules enforce still applies verbatim on the 2.3.0 path. With the five reuse cells verified, the 2.3.0 cells for Versions, Tokens, Commands, ChargingProfiles, HubClientInfo move ☐→☑.

The Locations module is now complete for 2.3.0 (#207 closes out #177). The last remaining verb — the eMSP-facing Receiver — now lands as http::locations_2_3_0_router over the v2_3_0 composites, exposing owner-qualified GET/PUT/PATCH (RFC 7396 merge-patch) at all three object levels on {country_code}/{party_id}/{location_id}[/{evse_uid}[/{connector_id}]], backed by the receiver surface (patch_location, put_evse/patch_evse, put_connector/patch_connector) added to the Locations230Config store from #200. It is kept a separate router from locations_2_3_0_sender_router (its flat 3-segment connector path would otherwise collide with the receiver's 3-segment owner path — the same split the 2.2 routers use). A client-owned Location push through this receiver preserves the additive 2.3.0 fields — parking_places/help_phone, Evse.parking/accepted_service_providers, and the ISO 15118 Connector.capabilities — in the store instead of dropping them through the 2.2.1 struct, while an unknown enum value is rejected on deserialize and a missing object maps to an explicit OCPI 2003 (UnknownLocation) rather than a silent empty 200. With types (#177) + client sender (#199) + server sender (#200) + server receiver all shipped, the 2.3.0 / Locations matrix cell moves ◑→☑.

The Credentials registration handshake now lands end-to-end for 2.3.0, completing the module (#206). On top of the hub_party_id fork of v2_3_0::Credentials, the client methods (OcpiClient::{register, get_credentials, update_credentials}_2_3_0) and the server receiver credentials_2_3_0_router + Credentials230Config now run the full Token A→B→C exchange over the fork. Because 2.3.0 reuses the 2.2.1 role-bearing Versions layer, the handshake shares the same registration fetch-back (VersionFetcher) and token-registry semantics as the 2.2.1 credentials_router — only the (de)serialize target changes — so a Hub partner's hub_party_id (the field by which a platform announces it is a Hub and states which party routes its message-routed traffic) survives the exchange instead of being silently dropped through the 2.2.1 shape. DELETE /credentials carries no body, so the version-agnostic delete_credentials is reused. The trust boundary holds: an over-length hub_party_id (CiString(5)) is rejected on deserialize before it reaches the store, and a non-hub 2.3.0 party (which omits the skip_serializing_if-guarded field) registers byte-identically to 2.2.1, so a 2.2.1 peer is never destabilised. With types + client + server receiver all shipped, the 2.3.0 / Credentials matrix cell moves ◑→☑.

M9 — the OCPI 3.0 forward-scaffold's first slice now lands: recognition-only (#219). Following the resolved 2.0 back-coverage precedent (#182) and the crate's own 3.0 contract (specs/ocpi/3.0/README.md"defer logic, not schema"), the version layer now recognises OCPI 3.0: VersionNumber::V3_0 parses/serialises "3.0", Display/as_str round-trip, and orders highest (V2_3_0 < V3_0). Crucially this is recognition, not support — because no supported set the crate ships includes V3_0, version negotiation can never select 3.0: a partner advertising only 3.0 degrades to no common version (→ an explicit UnsupportedVersion status_code), while a partner advertising 3.0 plus a mutual version still negotiates the highest mutual one, so a single forward-looking 3.0 entry in a /versions catalogue never breaks an otherwise-working handshake (the top-of-range mirror of the V2_0 degrade path, fenced by negotiate_disjoint_returns_none_for_3_0_only_partner + negotiate_ignores_forward_3_0_and_picks_highest_mutual in crates/ocpi-client/src/lib.rs). Deliberately no v3_0 type module, no ModuleID/endpoint change, and — exactly as with 2.0 — no 3.0 support-matrix column; the 3.0 implementation logic stays blocked-upstream until the spec is public.

How this repo is built

This repo develops itself. A nightly Claude remote routine picks one owner-approved GitHub issue, implements it on a branch, opens a PR, and marks it ready for review for the owner to merge under strict CI. See nightly/PLAYBOOK.md and CONTRIBUTING.md.

Governance: only the owner is trusted. The nightly bot's PRs are automatically marked ready for review (no auto-merge); the owner reviews and merges every PR once all required checks are green. See CONTRIBUTING.md.

Specifications

The OCPI specs are vendored under specs/ for reference. They are © EV Roaming Foundation and are not covered by this project's MIT license — see specs/NOTICE.md.

License

MIT — see LICENSE.

About

A modern, production-grade OCPI (Open Charge Point Interface) implementation in Rust: typed models, async client, and server handlers for every OCPI version.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors