codetics-ocpp-service 0.1.0

Codetics OCPP: Charging Station Management System (CSMS) service for OCPP 1.6J, 2.0.1 and 2.1, with TLS security profiles, smart charging, reservations and tariffs
Documentation

Codetics OCPP

A complete implementation of the Open Charge Point Protocol (OCPP-J 1.6, 2.0.1 and 2.1) in Rust, and a Charging Station Management System (CSMS) service built on it, written by Kevin Vavelin for Codetics.

  • Every message of the three versions as plain Rust types, with validation and typed dispatch; no OCPP library, no code generation.
  • Its own WebSocket stack (RFC 6455 + RFC 7692 compression), built for tens of thousands of concurrent stations.
  • TLS with OCPP Security Profiles 1–3, smart charging, reservations, tariffs and cost calculation, an HTTP API, PostgreSQL storage.

It can be used as is, as a whole service or crate by crate. Contributions are welcome (see CONTRIBUTING.md).

Crates

Crate Directory Role
codetics-ocpp-core crates/ocpp-core Version-independent OCPP-J: RPC framing (CALL / CALLRESULT / CALLERROR / CALLRESULTERROR / SEND), error codes and their 1.6 spellings, RFC 3339 DateTime, constraint validation, subprotocol negotiation, message ids
codetics-ocpp16 crates/ocpp16 OCPP 1.6 messages
codetics-ocpp201 crates/ocpp201 OCPP 2.0.1 messages (Edition 4)
codetics-ocpp21 crates/ocpp21 OCPP 2.1 messages (Edition 2), including SEND
codetics-ocpp-ws crates/ocpp-ws WebSocket: RFC 6455 framing, RFC 7692 permessage-deflate, handshake, minimal client
codetics-evse-id crates/evse-id E-mobility IDs (E/P/S + id) with the IDRR Annex 1 check character
codetics-ocpp-service . The CSMS service: handshake, TLS, sessions, per-version handlers, smart charging, reservations, tariffs, PostgreSQL, HTTP API

Each version crate is independent, and so are the version handlers of the service (src/ocpp/v16.rs, v201.rs, v21.rs). They share only version-neutral logic.

External crates: tokio, axum/hyper (HTTP), serde/serde_json, sqlx (PostgreSQL), flate2 (raw DEFLATE), openssl (TLS), argon2 (password hashing), tracing.

Version negotiation

The Sec-WebSocket-Protocol header is the source of truth:

  • OCPP-J 1.6 §3.1.2: "The exact OCPP version MUST be specified in the Sec-Websocket-Protocol field."
  • 2.0.1 and 2.1 Part 4 §3.1.2: "If the OCPP version is part of the OCPP-J endpoint URL it SHALL NOT determine the OCPP version to use."

The server picks the first version in the client's preference order that is both enabled (OCPP_VERSIONS) and allowed for that station (allowed_versions). If none matches, the handshake completes without Sec-WebSocket-Protocol and the connection is closed (Part 4 §3.2). Stations sold as "OCPP 1.8" negotiate ocpp1.6.

Connections and security

  • /<any prefix>/<identity>: the identity is the last path segment, percent-decoded (≤ 48 characters, no :). Unknown stations get HTTP 404: only stations registered through the API can connect.
  • Security Profiles (Part 2 §A00, same as the 1.6 security whitepaper), configured per station (security_profile): profile 1 = Basic auth over plain WebSocket, 2 = TLS + Basic auth, 3 = TLS + client certificate. A station connecting with another profile is refused (A00.FR.002). Without a profile, a password is required only if one is set.
  • TLS (OpenSSL): TLS 1.2+, the four cipher suites of A00.FR.318 (including TLS_RSA_WITH_AES_*_GCM, which rustls does not implement) plus ECDHE-RSA, an RSA and an ECDSA certificate side by side, no compression or renegotiation. Profile 3 checks the certificate chain against OCPP_TLS_CLIENT_CA, the O RDN against OCPP_CSO_NAME (A00.FR.404) and the CN against the station's identity or serial number (A00.FR.405); refusals are logged as InvalidChargingStationCertificate security events. Certificates are reloaded on SIGHUP.
  • Passwords: raw bytes (1.6 binary AuthorizationKey), its 40-character hex form, or a UTF-8 string (2.x); stored as Argon2id hashes, verified with bounded concurrency so reconnect storms cannot exhaust memory.
  • One task per connection multiplexes reads, the command queue, call timeouts, keep-alive pings and shutdown. At most one CSMS-initiated CALL is in flight per connection (§4.1.1).
  • permessage-deflate (required for a CSMS in 2.0.1/2.1) with pooled compressors: idle connections hold no compression state.
  • Before acceptance, transaction-related CALLs get SecurityError; never-booted stations are sent TriggerMessage(BootNotification). A newer connection for the same identity replaces the older one.
  • Per-version RPC errors: 1.6 spellings, 2.0.1 MessageTypeNotSupported / RpcFrameworkError with id "-1", 2.1 CALLRESULTERROR and SEND.

Measured on a laptop (client and server in one process): 10,000 stations connected and exchanged a heartbeat in about 6 s. Raise the file-descriptor limit (ulimit -n) for production loads.

Smart charging (1.6 §3.13, 2.x block K)

  • Registry of the charging profiles sent to (or reported by) each station, with the CSMS-side rules checked before sending: station max profile on connector/EVSE 0 only, TxProfile only during a transaction with its transactionId filled in (§5.16, K01.FR.03), no external-constraints profile from the CSMS (K01.FR.22), no conflicting stack levels (K01.FR.06/39), phaseToUse rules, 2.1 PriorityCharging and Dynamic profile rules.
  • Composite schedule computed by the CSMS (stacking, TxProfile over TxDefault, PriorityCharging, external limits, LocalGeneration, A↔W conversion), or requested from the station.
  • External limits (NotifyChargingLimit / ClearedChargingLimit), ReportChargingProfiles synchronisation, TxProfiles cleared when their transaction ends.
  • ISO 15118 charging needs (K15–K19): the CSMS answers and sends a TxProfile within the station limits.
  • OCPP 2.1 dynamic profiles (K28): UpdateDynamicSchedule pushes and PullDynamicScheduleUpdate answers.

Reservations (1.6 §5.13, 2.x H01–H04)

Reservation ids assigned by the CSMS; ReserveNow / CancelReservation; status followed from the station's answers, StartTransaction / TransactionEvent (used), ReservationStatusUpdate, Faulted/Unavailable connectors (1.6) and an expiry sweeper. Reservations by EVSE, any EVSE, or connector type (2.x), with group tokens.

Tariffs and cost (2.x block I)

OCPP 1.6 has no tariff mechanism; this applies to OCPP 2.0.1 and 2.1 stations, per cost_mode:

  • Central: the CSMS calculates. Tariff text in IdTokenInfo.personalMessage (I01), running cost on Updated events (I02) and final cost on Ended (I03, 0.00 for free sessions).
  • Local (2.1): SetDefaultTariff, GetTariffs, ClearTariffs, driver tariffs in AuthorizeResponse (I07–I10), ChangeTransactionTariff (I11); the station's costDetails are stored and recalculated by the CSMS for verification (I12).
  • Tariffs use the OCPP 2.1 TariffType and are immutable per id (I08.FR.08). The cost engine follows I12: conditions on local time of day, weekday, dates, energy, power, current and durations; a new charging period whenever the applicable prices change; stacked taxes; min/max cost; tariff changes without retroactivity. Local time comes from the system zoneinfo database, daylight saving time included.

EVSE IDs

IDRR format without the COUNTRY*OPERATOR* prefix: <E|P|S><id>. A station gets S + 6 base-36 characters + check character (e.g. S000001D); each EVSE gets E + station body + 2-digit EVSE number + check character. The check character uses the IDRR Annex 1 algorithm, which Annex 1 defines for contract IDs; here it covers the local part.

Running

export DATABASE_URL=postgres://localhost/ocpp
cargo run --release

Migrations run at startup (PostgreSQL with the pg_trgm and unaccent extensions). OpenSSL is linked from the system; build with --features vendored-openssl to compile it from source.

Variable Default
OCPP_LISTEN 0.0.0.0:9000 Plain WebSocket listener; none disables it
OCPP_TLS_LISTEN unset TLS listener (Security Profiles 2/3)
OCPP_TLS_CERT / OCPP_TLS_KEY Server certificate chain and key (PEM)
OCPP_TLS_CERT2 / OCPP_TLS_KEY2 Second pair, e.g. RSA next to ECDSA
OCPP_TLS_CLIENT_CA CA bundle for station certificates (profile 3)
OCPP_TLS_CIPHERS A00.FR.318 suites OpenSSL cipher list for TLS 1.2
OCPP_CSO_NAME unset Expected O RDN of station certificates
OCPP_MAX_CONNECTIONS 200000 Per listener
API_LISTEN 127.0.0.1:8080 Internal HTTP API
API_TOKEN unset Requires Authorization: Bearer … on /api/v1
DATABASE_URL required
DATABASE_MAX_CONNECTIONS 20
NODE_ID $HOSTNAME Stored as connected_node
OCPP_VERSIONS 1.6,2.0.1,2.1
HEARTBEAT_INTERVAL_SECS 300 Returned in BootNotification
CALL_TIMEOUT_SECS 30 CSMS → station calls
PING_INTERVAL_SECS 60 Server ping when idle; closes after 3× idle; 0 disables
MAX_MESSAGE_SIZE 4194304
WS_DEFLATE true
WS_FORCE_CLIENT_NO_CONTEXT_TAKEOVER false Saves memory per connection
OCPP_REQUIRE_PASSWORD false
AUTHORIZE_UNKNOWN_TOKENS false
COMMAND_QUEUE_SIZE 16 Per connection
AUTH_CONCURRENCY CPU count Parallel Argon2 verifications
NOMINAL_VOLTAGE 230 A ↔ W conversion in composite schedules
SMART_CHARGING_EV_NEEDS true Answer ISO 15118 charging needs with a TxProfile
ALLOW_BIDIRECTIONAL false Accept bidirectional transfer requests (2.1)
RESERVATION_SWEEP_SECS 30
DEFAULT_TIMEZONE UTC For stations without timezone
TZDIR /usr/share/zoneinfo
LOG_FORMAT / RUST_LOG text / info LOG_FORMAT=json for JSON logs

HTTP API (/api/v1)

POST /stations Register: ocpp_identity, name, latitude, longitude, address_line1/2, postal_code, city, region, country_code, timezone, allowed_versions, boot_policy, security_profile, cost_mode, evse_id_body, password or password_hex
GET /stations?q=&lat=&lon=&radius_km=&city=&postal_code=&country_code=&connected= Address search (accent- and case-insensitive), radius search, filters
GET/PATCH/DELETE /stations/{identity} Details include EVSEs, connector status and the live connection
PUT /stations/{identity}/password Set or remove credentials
GET /stations/{identity}/evses, /transactions?active=, /events?action=
POST /stations/{identity}/ocpp/{Action} Any CSMS→station action of the negotiated version, validated both ways
POST /stations/{identity}/commands/{reset, remote-start, remote-stop, unlock-connector, change-availability, trigger-message} Version-independent commands
GET/POST /stations/{identity}/charging-profiles, POST …/clear, POST …/sync, POST …/{profile_id}/dynamic-update Smart charging
GET /stations/{identity}/composite-schedule?evse_id=&duration=&unit=&source=csms|station Composite schedule
GET /stations/{identity}/charging-limits External limits
GET/POST /stations/{identity}/reservations, GET/DELETE …/reservations/{id} Reservations
GET/POST /stations/{identity}/tariffs, DELETE …/tariffs/{tariff_id} Default tariffs
GET /tariffs, GET/PUT/DELETE /tariffs/{tariff_id} Tariff registry
GET /transactions/{id}, GET …/cost, POST …/tariff Transactions, cost, tariff change (2.1)
PUT /id-tokens, GET /id-tokens, DELETE /id-tokens/{type}/{token} Authorization tokens and driver tariffs
GET /connections, GET /health

Command results: 200 with the station response, 400 invalid payload or rule violation, 409 conflict or station not connected, 502 CALLERROR or invalid response, 504 timeout.

Tests

cargo test --workspace                                                    # unit tests and README examples
TEST_DATABASE_URL=postgres://localhost/postgres cargo test --workspace    # + end-to-end tests (temporary databases)
E2E_STATIONS=10000 TEST_DATABASE_URL=… cargo test --release --test e2e many_concurrent -- --nocapture

Not implemented yet

  • Cross-node command routing: with several instances, a command for a station connected elsewhere returns 409 with connected_node.
  • Certificate management: no CA or OCSP integration (SignCertificate → Rejected, certificate status requests → Failed).
  • The OCPP 1.6 security whitepaper messages (SecurityEventNotification, SignedUpdateFirmware, …).

Author, license and credit

Codetics OCPP is written by Kevin Vavelin — codetics.fr.

Licensed under the Apache License, Version 2.0. Copyright 2026 Kevin Vavelin, Codetics.

You may use, modify and redistribute this work, including commercially, provided that every redistribution of it or of a derived work — forks included — keeps the copyright notices and includes the NOTICE file, which credits Kevin Vavelin / Codetics as the origin of the work.

OCPP is a specification of the Open Charge Alliance; this project is independent and not endorsed by it. See NOTICE for third-party attributions.