Track sessions and energy
OCPP functional blocks E — Transactions, and J — Metering.
Transactions are the record everything commercial is built on. This page covers how they are stored, how to query them, and how to derive energy from them — which is not a field you read.
A transaction is an event log#
Not a row that gets updated. A transaction has a Started event, any number
of Updated events, and — once it finishes — an Ended event. Meter readings
hang off those events.
curl https://api.flowion.io/v1/transactions/$TRANSACTION/events \
-H "Authorization: Bearer $TOKEN"
Each event carries:
| Field | Meaning |
|---|---|
event_type | Started, Updated or Ended |
seq_no | Position within the transaction, from 0 |
timestamp | When the station says it happened |
trigger_reason | Why the station sent it |
charging_state | Charging, EVConnected, SuspendedEV, SuspendedEVSE, Idle |
offline | Whether the station was disconnected and is replaying |
meter_values | Readings attached to this event |
Two of these behave differently depending on the station's OCPP version.
trigger_reason is 2.x only — CablePluggedIn, Authorized,
MeterValuePeriodic, ChargingStateChanged, RemoteStop and so on. A 1.6
station never says why it sent something, so this is empty for 1.6 fleets
rather than guessed.
seq_no is supplied by the station in 2.x, which lets a CSMS detect a
missing event. For 1.6 it is assigned in arrival order, so it orders events
but proves nothing about message loss.
offline is likewise always false for 1.6, even when the station
genuinely was disconnected — 1.6 has no way to say so.
Deriving energy#
There is deliberately no energy_consumed field, and no meter_start or
meter_stop. Energy comes from the meter readings, the same way for both
protocol versions.
Look for Energy.Active.Import.Register readings — a cumulative meter total,
in Wh. Consumption is the last minus the first:
energy = last(Energy.Active.Import.Register)
− first(Energy.Active.Import.Register)
OCPP 1.6 sends two bare integers at start and stop; those become ordinary readings tagged as the transaction's beginning and end, which is exactly what a 2.x station sends natively.
A register reading is not a session total. It is the meter's lifetime count. A session that shows
1204500did not deliver 1.2 MWh.
If a transaction has no energy readings at all, the station is not configured to sample them — see Read and change charger settings.
Querying#
Environment-wide, which is where most reporting starts:
curl "https://api.flowion.io/v1/environments/$ENV/transactions" \
-H "Authorization: Bearer $TOKEN"
Also scoped by charger and by connector:
curl https://api.flowion.io/v1/chargers/$CHARGER/transactions \
-H "Authorization: Bearer $TOKEN"
curl https://api.flowion.io/v1/connectors/$CONNECTOR/transactions \
-H "Authorization: Bearer $TOKEN"
A transaction summary carries both identifiers — numeric_id for a 1.6
station, text_id for 2.x — plus started_at, ended_at, the id_token
that started it, its charging_state and its stopped_reason.
ended_at is null while a session is still running. That is how you find
live sessions, and also the trap in any report that assumes every transaction
has an end.
Why a session stopped#
stopped_reason is worth handling properly, because some values indicate
problems rather than a driver leaving:
| Reason | Means |
|---|---|
Local | Normal — the driver ended it, or it was omitted entirely |
Remote | A remote stop command |
EVDisconnected | The cable came out |
DeAuthorized | The credential stopped being valid mid-session |
EmergencyStop, PowerLoss, GroundFault, OvercurrentFault | Faults worth alerting on |
EnergyLimitReached, TimeLimitReached, SOCLimitReached | A ceiling did its job |
Reboot, ImmediateReset | The station restarted |
One 1.6 quirk to know: 1.6's SoftReset and its Reboot both arrive as
Reboot, and are indistinguishable once stored. And 1.6's UnlockCommand
becomes Other, losing the reason entirely.
An omitted reason is normalised to Local, because both specifications permit
omitting it only in that case.
Standalone meter values#
Not every reading belongs to a session. A station may report its own meter with nobody charging:
curl https://api.flowion.io/v1/connectors/$CONNECTOR/meter-values \
-H "Authorization: Bearer $TOKEN"
curl https://api.flowion.io/v1/chargers/$CHARGER/meter-values \
-H "Authorization: Bearer $TOKEN"
A reading belongs to a transaction event or to a connector, never both. Which one is decided by what the station said on the wire, not inferred from whether a session happened to be running.
Main-meter readings are currently discarded. A reading the station reports for itself rather than for any connector — its whole-station meter — has nowhere to be stored yet and is logged rather than kept. If you need whole-site consumption, sum connectors.
Live updates instead of polling#
Transactions change while you watch. Rather than polling, subscribe:
GET /v1/environments/{environment_id}/live-events
A server-sent event stream of what an environment's chargers are doing now: connections, status changes, transaction lifecycle, meter values.
Two things to design for. Events are nudges to refetch, not the data itself — the payload tells you something changed, and you re-read what you care about. And delivery is best-effort: reconcile over the REST endpoints when you connect and after every reconnect, or a dropped event becomes a permanent gap in your view.
Meter-value events are coalesced into a short window, so a chatty station does not produce one event per reading.