DEV Community

EVChargingDevs
EVChargingDevs

Posted on

Getting Started with OCPP 1.6 Using a Simulated Charger

If you have ever plugged in an electric car at a public charger, two computers had a conversation you never saw: the charger (the charge point) and a server that manages it (the CSMS, Charging Station Management System). The language they speak is usually OCPP, the Open Charge Point Protocol.

This tutorial walks through that conversation hands-on with OCPP 1.6, the version most chargers in the field still run. You will start a CSMS on your laptop, connect a simulated charger, run a charging session from the dashboard, and then drive the same charger from the command line with curl. No hardware needed.

What OCPP is, in five minutes

OCPP is an open standard from the Open Charge Alliance. Version 1.6 (2015) is the workhorse of the industry. Versions 2.0.1 and 2.1 add more features, but the core ideas are the same.

Three ideas carry you a long way.

1. The charger connects to the server, not the other way around. A charge point opens a WebSocket to the CSMS and keeps it open. The URL ends with the charger's identity, for example:

ws://csms.example.com/ocpp/CS-0001
Enter fullscreen mode Exit fullscreen mode

During the WebSocket handshake the charger asks for the ocpp1.6 subprotocol, so the server knows which version it speaks. Because the connection stays open, the server can send commands to a charger that sits behind a mobile network or a firewall.

2. Every message is a small JSON array. OCPP 1.6 over WebSocket (often called OCPP-J) has three message types:

Type Meaning Shape
2 CALL, a request [2, "<messageId>", "<Action>", {payload}]
3 CALLRESULT, a successful answer [3, "<messageId>", {payload}]
4 CALLERROR, a failed answer [4, "<messageId>", "<errorCode>", "<description>", {details}]

The messageId ties an answer to its request. Either side can send a CALL: the charger reports things (it booted, a cable was plugged in, energy was delivered), and the server sends commands (start, stop, reset, change a setting).

3. A charger has connectors, and each connector has a status. Connector 1 is the first plug. Connector 0 means the charger as a whole. Statuses move through a small state machine:

Available -> Preparing -> Charging -> Finishing -> Available
Enter fullscreen mode Exit fullscreen mode

with side states such as SuspendedEV, Unavailable and Faulted.

The messages you will see in this tutorial:

Message Sent by Purpose
BootNotification Charger "I just started. Here is my vendor and model."
Heartbeat Charger "I am still here." Also syncs the clock.
StatusNotification Charger A connector changed status.
Authorize Charger "May this RFID tag (idTag) charge?"
StartTransaction Charger Charging started. The server answers with a transaction id.
MeterValues Charger Energy readings during the session.
StopTransaction Charger Charging stopped, with the final meter reading.
RemoteStartTransaction Server Start charging for an idTag, for example from an app.
RemoteStopTransaction Server Stop a transaction.
TriggerMessage, GetConfiguration Server Ask the charger to send a message now, or read its settings.

The lab

You need a CSMS to talk to and a charger to talk to it. For this tutorial I use EVtivity, a self-hosted CSMS that ships with a built-in charging station simulator, so the whole lab runs in Docker on one machine. Any CSMS and simulator would teach the same protocol; this one just saves setup.

You need Docker with Docker Compose, plus curl and jq for the last part.

git clone https://github.com/evtivity/evtivity-csms.git
cd evtivity-csms
docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Give it about a minute to start, then open the dashboard at http://localhost:7100 and sign in as admin@evtivity.local with the password admin123.

The stack starts two simulated chargers. CS-0001 speaks OCPP 1.6 and CS-0002 speaks OCPP 2.1. We will use CS-0001, which connects to ws://localhost:7103/CS-0001.

Step 1: BootNotification, and why a new charger waits

Open Stations, then CS-0001, and go to the OCPP Commands tab. At the bottom is the OCPP message log: every message between the charger and the server, newest first. Click a row to see its JSON.

The first thing a charger does after connecting is introduce itself:

[2, "a1b2c3", "BootNotification", {
  "chargePointVendor": "...",
  "chargePointModel": "...",
  "chargePointSerialNumber": "...",
  "firmwareVersion": "..."
}]
Enter fullscreen mode Exit fullscreen mode

The server answers with one of three statuses:

  • Accepted: the charger is registered and may operate.
  • Pending: the server knows about it but has not decided yet. The charger must not start normal operation and should retry after interval seconds.
  • Rejected: go away, retry later.

A fresh install answers Pending, because new chargers need an operator's approval first:

[3, "a1b2c3", { "status": "Pending", "currentTime": "2026-10-17T09:00:00Z", "interval": 300 }]
Enter fullscreen mode Exit fullscreen mode

This is a real-world pattern. A CSMS on the internet should not trust every device that knows its URL.

Click Approve on the station page. On its next BootNotification the charger receives Accepted, and the log fills up:

[3, "d4e5f6", { "status": "Accepted", "currentTime": "2026-10-17T09:05:00Z", "interval": 300 }]
Enter fullscreen mode Exit fullscreen mode

Two things follow:

  1. StatusNotification, once for connector 0 (the charger) and once per connector: {"connectorId": 1, "errorCode": "NoError", "status": "Available"}.
  2. Heartbeat every interval seconds (300 here). The empty request {} gets back {"currentTime": "..."}, which also keeps the charger's clock in sync.

The server chose the heartbeat interval, not the charger. The CSMS tells chargers how to behave, and OCPP gives it the messages to do so.

Step 2: Create an RFID tag

Drivers identify themselves with an idTag, usually the id of an RFID card, and the charger asks the server whether that tag may charge. An unknown tag is refused, so create one first.

Open Tokens and add a token with:

  • ID: TUTORIAL-001
  • Type: RFID (ISO14443)
  • Driver: the demo driver, driver@evtivity.local

Keep idTags at 20 characters or fewer. OCPP 1.6 limits them to 20.

Step 3: A charging session, message by message

Back on CS-0001, open the Simulate tab. It acts as the car and the driver. Enter TUTORIAL-001 as the ID token and click through the steps, checking the message log after each one.

Plug in. The charger notices a cable:

[2, "...", "StatusNotification", { "connectorId": 1, "errorCode": "NoError", "status": "Preparing" }]
Enter fullscreen mode Exit fullscreen mode

Start charging (the driver taps the card). Three things happen in order.

First the charger asks whether the tag is allowed:

[2, "...", "Authorize", { "idTag": "TUTORIAL-001" }]
[3, "...", { "idTagInfo": { "status": "Accepted" } }]
Enter fullscreen mode Exit fullscreen mode

Then it starts a transaction and reports its meter reading in Wh:

[2, "...", "StartTransaction", { "connectorId": 1, "idTag": "TUTORIAL-001", "meterStart": 0, "timestamp": "..." }]
[3, "...", { "transactionId": 42, "idTagInfo": { "status": "Accepted" } }]
Enter fullscreen mode Exit fullscreen mode

The server assigns the transactionId. From here on, both sides use it to refer to this session.

Finally the connector status changes to Charging.

While charging, the charger sends MeterValues every few seconds (the MeterValueSampleInterval setting), each tagged with the transaction id:

[2, "...", "MeterValues", {
  "connectorId": 1,
  "transactionId": 42,
  "meterValue": [{ "timestamp": "...", "sampledValue": [{ "value": "1520", "measurand": "Energy.Active.Import.Register", "unit": "Wh" }] }]
}]
Enter fullscreen mode Exit fullscreen mode

The Sessions page in the dashboard shows the energy growing. That number comes straight from these messages.

Stop charging. The charger closes the transaction with its final meter reading:

[2, "...", "StopTransaction", { "transactionId": 42, "meterStop": 3150, "timestamp": "...", "reason": "Local" }]
Enter fullscreen mode Exit fullscreen mode

The connector then moves to Finishing. Unplug returns it to Available.

The energy for the session is meterStop - meterStart. Billing systems are built on this subtraction, which is why chargers report a cumulative meter register rather than the energy of each session.

Step 4: The same thing from the command line

Everything the dashboard does goes through a REST API, so you can drive the charger with curl. The API runs on port 7102.

Get an access token. It is valid for an hour:

TOKEN=$(curl -s localhost:7102/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@evtivity.local","password":"admin123"}' | jq -r .token)

AUTH="Authorization: Bearer $TOKEN"
Enter fullscreen mode Exit fullscreen mode

Look at the charger:

curl -s "localhost:7102/v1/stations?search=CS-0001" -H "$AUTH" \
  | jq '.data[] | {id, stationId, ocppProtocol, isOnline, status}'
Enter fullscreen mode Exit fullscreen mode

Note the id field (something like sta_000000000001). The station URLs below use it.

Send a TriggerMessage. This asks the charger to send a message now instead of waiting:

curl -s localhost:7102/v1/ocpp/commands/v16/TriggerMessage -H "$AUTH" \
  -H 'Content-Type: application/json' \
  -d '{"stationId":"CS-0001","requestedMessage":"Heartbeat"}' | jq
Enter fullscreen mode Exit fullscreen mode

The response field holds the charger's own answer (Accepted), and a fresh Heartbeat appears in the log.

Read a configuration key. In OCPP 1.6, chargers keep their settings as key-value pairs:

curl -s localhost:7102/v1/ocpp/commands/v16/GetConfiguration -H "$AUTH" \
  -H 'Content-Type: application/json' \
  -d '{"stationId":"CS-0001","key":["HeartbeatInterval","MeterValueSampleInterval"]}' | jq .response
Enter fullscreen mode Exit fullscreen mode

Start a session remotely, the way a mobile app does. First plug the simulated car in:

curl -s localhost:7102/v1/css/actions/plugIn -H "$AUTH" \
  -H 'Content-Type: application/json' \
  -d '{"stationId":"CS-0001","evseId":1}'
Enter fullscreen mode Exit fullscreen mode

Then send RemoteStartTransaction:

curl -s localhost:7102/v1/ocpp/commands/v16/RemoteStartTransaction -H "$AUTH" \
  -H 'Content-Type: application/json' \
  -d '{"stationId":"CS-0001","connectorId":1,"idTag":"TUTORIAL-001"}' | jq .response
Enter fullscreen mode Exit fullscreen mode

Watch the log. The server sends a CALL, the charger answers {"status": "Accepted"}, and then the charger itself sends StartTransaction, just like in Step 3. An accepted RemoteStartTransaction only means "I will try". The session exists once StartTransaction arrives.

Find the transaction and stop it:

TX=$(curl -s "localhost:7102/v1/sessions?status=active" -H "$AUTH" \
  | jq -r '.data[] | select(.stationName=="CS-0001") | .transactionId')

curl -s localhost:7102/v1/ocpp/commands/v16/RemoteStopTransaction -H "$AUTH" \
  -H 'Content-Type: application/json' \
  -d "{\"stationId\":\"CS-0001\",\"transactionId\":$TX}" | jq .response
Enter fullscreen mode Exit fullscreen mode

The charger accepts, then sends StopTransaction with "reason": "Remote".

Read the message log from the API:

STATION_ID=sta_000000000001   # the id from the stations call above
curl -s "localhost:7102/v1/stations/$STATION_ID/ocpp-logs?limit=10" -H "$AUTH" \
  | jq '.data[] | {createdAt, direction, action, messageType}'
Enter fullscreen mode Exit fullscreen mode

messageType is the same 2, 3 or 4 from the table at the top.

What you have learned

  • A charger opens a WebSocket to the CSMS and keeps it open. The URL carries its identity, and the subprotocol carries the OCPP version.
  • Every message is a CALL, a CALLRESULT or a CALLERROR, matched by message id.
  • BootNotification registers a charger, and the server can hold it at Pending.
  • A session is Authorize, StartTransaction, MeterValues and StopTransaction. The server-issued transaction id ties them together.
  • Remote commands only ask. The charger's own messages report what actually happened.

The OCPP 1.6 specification is free to download from the Open Charge Alliance and reads well once these basics are in place.


This article was written with the help of AI and reviewed by the author.

Top comments (0)