Connect a simulated charger
The fastest way to learn this platform is to drive a charging station through a whole session — boot, plug in, authorize, charge, stop — and watch what arrives. A simulator lets you do that in a couple of minutes, with no hardware, no site visit and no vehicle.
It is also how you should test your integration. Real chargers are slow to reset, awkward to put into error states, and generally installed somewhere inconvenient.
The simulator is a local development service today. It is not part of the hosted platform, has no authentication on its own API, and must run where it can reach both the OCPP gateway and the database. Run it yourself, as described below. Exposing simulated fleets in the console is planned.
Running it#
The simulator ships with the platform's source. Started as part of the local
development stack, it listens on port 8082:
docker compose up -d # Postgres and the identity provider
pnpm dev # gateway, API, console, auth stub, simulator
It takes two things from its environment:
| Variable | Purpose |
|---|---|
OCPP_GATEWAY_WS_URL | Where to dial, e.g. ws://localhost:8081 |
DATABASE_URL | Needed to seed credentials and resolve the connection URL |
Without DATABASE_URL it can still connect to a gateway, but it cannot seed
credentials or work out which organization a charger belongs to — so it falls
back to a placeholder slug that only authenticates against a gateway that does
not check. For anything realistic, give it the database.
Check it is up:
curl http://localhost:8082/healthz
Registering a charger#
Registering a simulated charger both creates it and connects it — there is no separate connect step.
curl -X POST http://localhost:8082/chargers \
-H "Content-Type: application/json" \
-d '{
"charge_point_id": "CP-01",
"authorization_key": "0001020304050607FFFFFFFFFFFFFFFFFFFFFFFF",
"environment_id": "'"$ENV"'"
}'
{
"charge_point_id": "CP-01",
"credentials_seeded": true,
"state": { "booted": false, "cable_connected": false }
}
environment_id is what makes this realistic. With it, the simulator looks
up the environment's organization, builds the correct connection URL —
/ocpp/{org_slug}/{charge_point_id}, or with the environment slug when it is
not the default — and catalogues the charger so it appears in the environment.
Without it, none of that happens.
credentials_seeded: true means it wrote the charger's credentials to the
database itself. That is why you can pass any authorization_key you like
here: the simulator makes the credential true rather than proving it knows one.
Against a real gateway you would use the key
provisioning returned.
A 502 means it could not reach the gateway. A 409 means that charge point
id is already registered with the simulator.
Driving a session#
Each action sends one OCPP message and returns the resulting state. The
simulator enforces the real state machine, so calling them out of order gives
you 409 — which is itself a useful way to learn the protocol's ordering
rules.
SIM=http://localhost:8082/chargers/CP-01/actions
curl -X POST $SIM/boot
curl -X POST $SIM/connect-cable
curl -X POST $SIM/authorize -H "Content-Type: application/json" \
-d '{"id_tag": "045C7A2B4F1D80"}'
curl -X POST $SIM/start-charging
curl -X POST $SIM/send-meter-value -H "Content-Type: application/json" \
-d '{"watt_hours": 1500}'
curl -X POST $SIM/stop-charging
curl -X POST $SIM/disconnect-cable
| Action | OCPP message | Refuses unless |
|---|---|---|
boot | BootNotification | — |
connect-cable | StatusNotification (Preparing) | booted |
authorize | Authorize | cable plugged in |
start-charging | StartTransaction | cable plugged in, token authorized, no transaction running |
send-meter-value | MeterValues | a transaction is running |
stop-charging | StopTransaction | a transaction is running |
disconnect-cable | StatusNotification (Available) | cable plugged in, no transaction running |
power-off | none | — |
send-meter-value takes watt_hours to add to the station's running meter
total, not an absolute reading — which matches how a real meter behaves.
stop-charging accepts a ?reason= query parameter carrying any OCPP 1.6
stop reason, such as EVDisconnected or Remote. It defaults to Local. An
unrecognised value returns 400.
power-off resets the simulated station's state without sending anything —
the equivalent of pulling its power. Use it to test how your integration
handles a station that vanishes mid-session.
Every action returns the OCPP outcome alongside the new state, so you can see what the CSMS answered:
{
"charger_id": "CP-01",
"action": "start-charging",
"ocpp_outcome": { "...": "the CSMS's response" },
"charger_state": { "...": "the simulated station afterwards" }
}
Watching from the other side#
While the session runs, the management API shows the same story from the CSMS's point of view:
curl https://api.flowion.io/v1/chargers/$CHARGER \
-H "Authorization: Bearer $TOKEN"
Its status becomes online after boot, boot_report fills in, and its
EVSEs and connectors appear. Connector status tracks the session:
Available → Preparing → Charging → Finishing → Available.
Transactions and meter values accumulate as you drive the actions. This is the loop worth spending ten minutes on — it makes Lifecycle of a charging session concrete in a way reading cannot.
Inspecting simulated chargers#
curl http://localhost:8082/chargers # all of them
curl http://localhost:8082/chargers/CP-01 # one, with its state
Simulated chargers are catalogued separately from real ones, so a simulated fleet never contaminates your real charger inventory.
What it is good for#
Learning the protocol. The 409s teach the ordering rules faster than the
specification does.
Testing your integration. Drive sessions on demand, in a loop, without touching hardware.
Reproducing failures. power-off mid-transaction, or a stop reason you
have never handled, are one call away and awkward to arrange in a car park.
Load generation. Register many chargers against one environment to see how your integration behaves with a fleet rather than a unit.
What it is not#
It is not a conformance test. It sends well-formed messages in a sensible order, which is precisely what real chargers often do not. Firmware quirks, duplicate messages, missing fields, clocks that are hours out and connections that die mid-Call are the things that will actually break your integration, and none of them appear here.
Test with the simulator; commission with real hardware.
Next: Connect a real charger.