How Flowion maps onto OCPP
You have now met two vocabularies: OCPP's, and Flowion's. This page is the boundary between them — what belongs to the protocol, what is ours, and where the translation between them is imperfect.
Read it once before you design anything against the API. The places where the mapping is lossy are places your own model has to make a decision too.
The dividing line#
Everything a charging station knows about is OCPP's. Everything above it is ours.
Organization ─┐
└─ Environment │ ours: accounts, isolation, physical grouping,
├─ Site │ electrical topology. OCPP has no idea these exist.
│ └─ Circuit ─┘
└─ Charger ─┐
└─ EVSE │ OCPP's: the 3-tier model, transactions,
└─ Connector │ credentials, meter readings.
┘
A charger is where the two meet. Above it, concepts exist because operators need them. Below it, concepts exist because the specification defines them.
Ours, and not in OCPP at all:
| Concept | Why it exists |
|---|---|
| Organization | The account. Billing and membership. |
| Environment | The isolation boundary — prod, staging, dev fleets. |
| Site | A physical location. OCPP calls this a Charging Location and explicitly gives it no protocol meaning. |
| Circuit | The electrical wiring chargers hang off, as a tree. Needed for load management; entirely outside the protocol. |
Getting started covers these in detail.
The API never names an OCPP message#
This is the design decision everything else follows from.
You will not find StartTransaction, RemoteStopTransaction or
TransactionEvent in the REST API. You ask for a charger to be reset; what
goes on the wire is the gateway's problem, and depends on what that particular
station negotiated.
The benefit is direct: you write one integration for a mixed fleet. A 1.6 station and a 2.1 station are the same kind of object to you.
The cost is that when something is genuinely version-specific, we have to surface it rather than pretend. Those cases are listed below, and there are fewer of them than you might expect.
The model is shaped after OCPP 2.0.1#
Flowion's internal model follows 2.x's structure, not 1.6's, and 1.6 is mapped into it.
That direction is deliberate. In every case where the two differ, 1.6's shape is a special case of 2.x's — so mapping 1.6 upward means synthesizing structure, never discarding it. Doing it the other way would mean inventing 2.x structure out of 1.6 messages that never carried it, which is the harder direction and loses information.
The practical consequences for a 1.6 station:
It gets one EVSE. 1.6 has no EVSE tier, so every 1.6 charger is modelled as
a single EVSE (id 1) holding all its connectors. This is a synthesized object
— the station never mentioned it — but it means your code walks the same
hierarchy for every charger.
Its status is split in two. 1.6's single connector status conflates availability with what the connector is doing. We store 2.x's separation: a connector status, plus a charging state on the transaction.
Its transaction becomes an event log. StartTransaction, MeterValues and
StopTransaction become Started, Updated and Ended events on one
transaction — the same shape a 2.x station produces directly.
Its meter readings are not special-cased. There are deliberately no
meterStart/meterStop fields anywhere in our model. 1.6's two bare Wh
integers become ordinary meter readings tagged as the transaction's beginning
and end, which is exactly what 2.x sends. Energy consumed is derived the same
way for both versions.
Two identities per object#
Every entity has a UUID that is ours, and the identifier the protocol puts on the wire. They are not interchangeable, and the distinction matters most for transactions.
| Who allocates the id | Type | |
|---|---|---|
| 1.6 | the CSMS, returned in the start response | integer |
| 2.x | the charging station, present from the first event | string (≤ 36 chars) |
So a transaction id is not one thing. A 2.x station may perfectly well choose
the string "90210", and that is not the same as 1.6's integer 90210 — it
just looks like it. We keep the two forms distinct rather than coercing them,
and a digits-only 2.x id deliberately will not answer to a request for a
numeric one. Answering a 1.6 station with a 2.x id would be wrong even when it
parses.
Use our UUIDs. They are stable, unambiguous, and the same shape for every charger. Protocol ids are exposed because you will need them when reading a station's own logs, not because you should key on them.
Where a command cannot be sent#
The model is wider than any single wire. Some commands simply cannot be expressed against a 1.6 station, and the honest answer is a failure rather than an approximation.
| Request | Why 1.6 cannot carry it |
|---|---|
| Stop a transaction identified by a string id | 1.6's transaction id is an integer |
| Remote start with a credential over 20 characters | 1.6 caps idTag at 20 characters |
Trigger a message on connector 0 | 1.6 forbids it |
The alternatives — inventing an integer, truncating the credential, sending a payload the station will reject — would all name something, just not what you asked for. So nothing goes on the wire and you get an error saying the wire cannot express it.
Expect this pattern to recur wherever a 2.x-shaped concept is strictly wider than 1.6's.
Where information is genuinely lost#
Mapping 1.6 into a 2.x-shaped model cannot round-trip the handful of values 2.x removed. Every case is deliberate and recorded:
| 1.6 value | Becomes | Consequence |
|---|---|---|
Stop reason HardReset | ImmediateReset | 2.x's name for the same thing — nothing lost |
Stop reason SoftReset | Reboot | Collides with 1.6's own Reboot; the two are indistinguishable once stored |
Stop reason UnlockCommand | Other | No 2.x equivalent; the reason is lost |
Measurand Temperature | dropped | No 2.x equivalent |
Measurand RPM | dropped | No 2.x equivalent |
| Signed meter data | no decimal value | The blob is preserved, but it is not a number |
Dropped readings are reported and logged, never silently discarded. Temperature and RPM are diagnostic rather than billing-relevant, which is the only reason dropping them is tolerable.
There is a matching narrowing in the other direction. Five authorization
verdicts that 2.x defines — including "no credit" and "not at this location" —
have no 1.6 representation, and collapse to Blocked on the wire. We store
the verdict you gave, not the one the station saw, so your records keep the
real reason even when the station could not be told it.
Fields a 1.6 station never fills in#
Modelled as absent rather than defaulted, so "1.6 did not say" stays distinguishable from a real value:
- Credential type. Never reported by 1.6. Notably not guessed as "RFID", which would assert an encoding the station never mentioned.
- Trigger reason. 2.x says why it sent a transaction event; 1.6 does not.
- Offline flag. 1.6 has no way to say "this happened while I was disconnected", so it always reads as false — even when the station genuinely was offline.
- Sequence numbers. In 2.x the station supplies these, which lets a CSMS detect a missing event. For 1.6 we assign them in arrival order, so they order events but prove nothing about message loss.
If you are building anything that depends on these, check whether your fleet can actually supply them.
One field is normalised rather than left empty: a missing stop reason becomes
Local, because both specifications permit omitting it only in that case.
What is not stored yet#
Stated plainly so you do not go looking:
- Main-meter readings. A
MeterValueson connector0belongs to the station rather than to any connector or session, and there is nowhere to put it yet. These are logged and discarded. - Boot history. Only the latest boot report is kept, so a firmware change or a reboot loop leaves no trail.
- Command follow-ups. A remote start is acknowledged, and the resulting transaction arrives separately with nothing linking the two. The same is true of triggered messages — nothing tracks whether an accepted trigger was ever honoured.
A complete, maintained list of limitations is planned under Troubleshooting.
Where to go next#
You have finished Orientation. The vocabulary and the model should now read as familiar rather than arbitrary.
- Quickstart — get a charger reporting telemetry.
- API reference — every endpoint, generated from the API's own OpenAPI document.