Introduction
API guide
  • AuthenticationSoon
  • Your first API callSoon
  • Interactive API referenceSoon
  • Roles and permissionsSoon
  • Errors, pagination and versioningSoon
  • Live updatesSoon
  • Sending commands to a chargerSoon
  • Rate limitsSoon
Troubleshooting
  • My charger will not connectSoon
  • It connects, then dropsSoon
  • A command timed out or returned 409Soon
  • A card is not being authorizedSoon
  • My transaction looks wrongSoon
  • Meter values are missing or sparseSoon
  • Reading the OCPP message logSoon
  • Known limitationsSoon
Reference
  • GlossarySoon
  • Endpoint indexSoon
  • OCPP message supportSoon
  • Status and error codesSoon
  • ChangelogSoon
Flowion Docs

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:

VariablePurpose
OCPP_GATEWAY_WS_URLWhere to dial, e.g. ws://localhost:8081
DATABASE_URLNeeded 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
ActionOCPP messageRefuses unless
bootBootNotification
connect-cableStatusNotification (Preparing)booted
authorizeAuthorizecable plugged in
start-chargingStartTransactioncable plugged in, token authorized, no transaction running
send-meter-valueMeterValuesa transaction is running
stop-chargingStopTransactiona transaction is running
disconnect-cableStatusNotification (Available)cable plugged in, no transaction running
power-offnone

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: AvailablePreparingChargingFinishingAvailable.

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.