PHP2Shell alert: critical PHP flaws CVE-2026-109618 / 109619 — check your servers and get patched fast
HashTech Systems
EV Charging
Part 10 of 10

Mapping OCPP to OCPI: How a CSMS Bridges Chargers and Roaming

How a CSMS translates OCPP 1.6 and 2.0.1 events into OCPI 2.2.1: connector status to EVSE status, transactions to Sessions and CDRs, commands and auth.

HashTech Engineering · October 26, 2026 · 7 min read

OCPP and OCPI were designed by different groups for different jobs, and nothing in either specification says how to connect them. Yet every CPO that roams has to: chargers speak OCPP to the CSMS, and the CSMS speaks OCPI to partners. This final part of the series is about that translation layer, the decisions it forces, and the places it usually breaks.

This is part 10 of the OCPI 2.2.1 series. It assumes familiarity with OCPP; if you need a refresher, start with the OCPP 1.6 tutorial or the OCPP 2.0.1 tutorial.

Architecture: never translate message by message

The tempting design is a direct relay: an OCPP StatusNotification comes in, an OCPI PATCH goes out. It works in a demo and fails in production, because the two protocols have different identity models, different timing and different failure modes. The robust design puts a domain model in the middle:

  1. OCPP handlers update the CSMS's own model of stations, EVSEs, connectors, transactions and meter readings.
  2. OCPI publishers watch that model for changes and decide what to send to which partner, with their own queues, retries and rate limits.
  3. OCPI receivers (tokens, commands, charging profiles) act on the model, which then drives OCPP calls.

This also makes mixed fleets manageable: 1.6 and 2.0.1 chargers both feed the same model, and OCPI never needs to know which protocol a charger speaks. See CSMS architecture at scale for how this fits into the wider platform.

Identity: stations, EVSEs and connectors

OCPI's hierarchy is Location, EVSE, Connector, and status lives on the EVSE. OCPP 2.0.1 has a near-identical Charging Station, EVSE, Connector model, so evseId maps naturally to an OCPI EVSE. OCPP 1.6 only has a charge point and numbered connectors, and a 1.6 "connector" is usually one charging point that can serve one car, which is an OCPI EVSE.

OCPPOCPI
Charge point / Charging StationUsually part of a Location; several stations can share one Location
OCPP 1.6 connectorId 1..nOne EVSE each (with one or more OCPI Connectors), unless the hardware shares one power module between outlets
OCPP 2.0.1 evseIdEVSE
OCPP 2.0.1 connectorId within an EVSEConnector
OCPP 1.6 connectorId 0No OCPI equivalent; station-wide status that you fold into each EVSE

A dual-outlet 1.6 charger that can only charge one car at a time is the awkward case. Publishing it as one EVSE with two Connectors is more accurate for drivers, but then you must merge two OCPP connector statuses into one EVSE status. Keep the OCPI uid stable and independent of OCPP numbering, so a hardware swap does not change it.

Status mapping

OCPP 1.6 ChargePointStatusOCPP 2.0.1 ConnectorStatusEnumTypeOCPI EVSE status
AvailableAvailableAVAILABLE
PreparingOccupiedCHARGING or BLOCKED (see below)
Charging, SuspendedEV, SuspendedEVSEOccupiedCHARGING
FinishingOccupiedCHARGING or BLOCKED
ReservedReservedRESERVED
UnavailableUnavailableINOPERATIVE
FaultedFaultedOUTOFORDER
Station offline / no heartbeatStation offlineUNKNOWN after a grace period

OCPI has no exact equivalent of Preparing or Finishing. What matters for drivers is that the EVSE is not free, so do not map them to AVAILABLE. Some CPOs use CHARGING for anything with a vehicle connected; others use BLOCKED for a plugged-in car not yet authorized. Pick one rule, apply it consistently, and document it for partners. Also remember that Preparing is sometimes reported when a driver merely presents a card without plugging in, which is a good reason to debounce before pushing. Status semantics on the OCPP side are covered in OCPP 1.6 StatusNotification and OCPP 2.0.1 boot and provisioning.

Authorization

When a charger sends OCPP Authorize (or a 1.6 StartTransaction with an unknown idTag), the CSMS has to decide whether the idTag belongs to a roaming partner:

  1. Normalise the idTag and look it up in the CSMS's own customers first, then in tokens received via OCPI.
  2. Apply the token's whitelist value (see Part 5): answer locally, or call the eMSP's authorize endpoint.
  3. Store the returned authorization_reference against the EVSE and token, because OCPP 1.6 has nowhere to carry it.
  4. Answer the charger.
OCPI AllowedType / statusOCPP 1.6 AuthorizationStatusOCPP 2.0.1 AuthorizationStatusEnumType
ALLOWEDAcceptedAccepted
BLOCKEDBlockedBlocked
EXPIREDExpiredExpired
NO_CREDITInvalidNoCredit
NOT_ALLOWEDInvalidNotAllowedTypeEVSE, NotAtThisLocation or NotAtThisTime if known, else Invalid
Status 2004 (unknown token)InvalidUnknown

For tokens with whitelist ALWAYS or ALLOWED, consider pushing them to chargers' local authorization lists so sessions can start while the charger is offline; never do this for NEVER. The OCPP mechanics are in OCPP 1.6 Authorize and id tags and OCPP 2.0.1 authorization.

Transactions to Sessions and CDRs

OCPP eventOCPI action
Successful authorization or accepted remote start, before energyOptional: PUT Session with status: PENDING
1.6 StartTransaction / 2.0.1 TransactionEvent StartedPUT Session, status: ACTIVE, kwh: 0, auth_method and authorization_reference set
MeterValues / TransactionEvent Updated with meter dataThrottled PATCH of kwh, charging_periods, total_cost
EV stops drawing power (SuspendedEV, 2.0.1 chargingState change)Start a new charging period; time now counts as PARKING_TIME
1.6 StopTransaction / 2.0.1 TransactionEvent EndedPATCH Session status: COMPLETED, end_date_time; then compute and POST CDR
Transaction never really started (no energy, rejected)PATCH Session status: INVALID, no CDR (or per partner agreement)

A few details make the difference between CDRs that reconcile and CDRs that get disputed:

  • Energy comes from meter readings, not from adding up samples. 1.6 meterStop - meterStart (Wh) divided by 1000 gives kWh; in 2.0.1 use the Energy.Active.Import.Register values at start and end. Background on measurands is in OCPP 1.6 meter values and OCPP 2.0.1 TransactionEvent.
  • Charging vs parking time needs a signal. OCPP 1.6 reports SuspendedEV via StatusNotification; 2.0.1 reports chargingState in TransactionEvent. Some 1.6 chargers never report SuspendedEV, so a fallback based on power dropping near zero in MeterValues is often needed to split TIME from PARKING_TIME.
  • Timestamps from the charger. Use the charger's timestamps for start and stop, not the CSMS receive time, but sanity-check them; chargers with a dead RTC report dates in 1970 or 2000. Time sync via Heartbeat and BootNotification is covered in OCPP 1.6 BootNotification and Heartbeat.
  • Offline transactions. A charger that was offline replays StartTransaction/StopTransaction later. The OCPI Session may then go straight from nothing to COMPLETED, which is allowed; just make sure the CDR is still produced. Queuing behaviour is in OCPP 1.6 start and stop transaction.
  • Session IDs. Use your own stable OCPI session ID, not the OCPP transactionId. 1.6 transaction IDs are integers assigned per CSMS and can collide across migrations; 2.0.1 IDs are generated by the charger.

Commands and charging profiles

OCPIOCPP 1.6OCPP 2.0.1
START_SESSIONRemoteStartTransaction (idTag, connectorId)RequestStartTransaction (idToken, evseId, remoteStartId)
STOP_SESSIONRemoteStopTransaction (transactionId)RequestStopTransaction (transactionId)
RESERVE_NOWReserveNowReserveNow
CANCEL_RESERVATIONCancelReservationCancelReservation
UNLOCK_CONNECTORUnlockConnectorUnlockConnector
Charging Profiles PUTSetChargingProfile with TxProfileSetChargingProfile with TxProfile
Charging Profiles GETGetCompositeScheduleGetCompositeSchedule
Charging Profiles DELETEClearChargingProfileClearChargingProfile

For START_SESSION on OCPP 1.6, the idTag sent to the charger has to be something the charger will later report in StartTransaction, so the CSMS can match the transaction to the command. App-user UIDs longer than the 20-character idTag limit need a short alias that the CSMS maps back. In 2.0.1, remoteStartId exists exactly for this correlation and comes back in TransactionEvent. Either way, store the eMSP's authorization_reference against the pending start and copy it into the Session. The OCPP side is detailed in OCPP 1.6 remote start and stop and OCPP 1.6 smart charging.

Tariffs and cost

OCPP 1.6 has no tariff concept at all, so roaming cost is calculated entirely in the CSMS from the OCPI tariff, the charging periods and the meter data. OCPP 2.0.1 can show costs on the charger (see OCPP 2.0.1 display messages and costs), but the CDR amount should still come from the same CSMS calculation that produced the OCPI tariff, so the eMSP can reproduce it. One tariff engine, used for live session cost, CDR cost and anything displayed on the charger, avoids three slightly different answers.

Testing the bridge

  • Drive end-to-end scenarios with a charge point simulator (see testing OCPP with a charge point simulator) and assert on the OCPI messages produced, not just the OCPP ones.
  • Include offline replays, a charger with a wrong clock, a session crossing a tariff boundary, SuspendedEV idle time, and a remote start the driver never plugs in for.
  • Compare your CDR totals against an independent calculation from the published tariff for every test session.
  • Monitor the OCPI outbox: undelivered sessions and CDRs per partner should be close to zero at all times.

Key takeaways

  • Bridge OCPP and OCPI through a domain model, not message-by-message relaying.
  • OCPP 1.6 connectors usually become OCPI EVSEs; keep OCPI IDs independent of OCPP numbering.
  • Never map Preparing/Finishing to AVAILABLE; debounce status before pushing.
  • Store authorization_reference and remote start correlation in the CSMS, because OCPP 1.6 cannot carry them.
  • Compute energy from register readings, split charging and parking time explicitly, and use one tariff engine for every cost figure.

That concludes the OCPI series. For the device side in depth, continue with the OCPP 1.6 and OCPP 2.0.1 tutorials. HashTech builds OCPP-certified CSMS platforms with OCPI roaming integration; see our EV charging work or get in touch.

Let's Talk

Ready to build something that lasts?

Whether it's a charging network, an AI rollout, or a core enterprise system — let's talk about what you're building.