PHP2Shell alert: critical PHP flaws CVE-2026-109618 / 109619 — check your servers and get patched fast
HashTech Systems
EV Charging
Part 9 of 11

OCPP 2.0.1 Display Messages, Tariffs and Running Cost

Showing tariffs, running cost and custom messages on OCPP 2.0.1 stations: SetDisplayMessage, personalMessage, CostUpdated and totalCost.

HashTech Engineering · October 26, 2026 · 4 min read

Drivers want to know what they will pay before they charge and what they paid when they leave, and in many markets price transparency is a regulatory requirement (see NEVI and AFIR requirements). OCPP 1.6 had no standard way to put text on a station screen. 2.0.1 adds two functional blocks for it: O (Display Message) and I (Tariff and Cost).

Display messages (block O)

The CSMS can store messages on the station for display, each with a priority, an optional state in which it applies, an optional validity window and an optional transaction binding.

json
[2, "sdm-11", "SetDisplayMessage", {
  "message": {
    "id": 14,
    "priority": "NormalCycle",
    "state": "Idle",
    "startDateTime": "2026-10-26T00:00:00.000Z",
    "endDateTime": "2026-11-30T23:59:59.000Z",
    "message": {
      "format": "UTF8",
      "language": "en",
      "content": "0.45 EUR/kWh, idle fee 0.10 EUR/min after 30 min"
    }
  }
}]

[3, "sdm-11", {
  "status": "Accepted"
}]
FieldValues
priorityAlwaysFront (shown permanently, overrides others), InFront (shown before normal-cycle messages), NormalCycle (rotates with others)
stateCharging, Faulted, Idle, Unavailable; omit for any state
message.formatASCII, HTML, URI, UTF8
transactionIdShow only during that transaction (use case O02)
displayA ComponentType to target a specific display on multi-display stations

The response status tells you exactly what the station could not do: Accepted, NotSupportedMessageFormat, NotSupportedPriority, NotSupportedState, UnknownTransaction or Rejected. Read DisplayMessageCtrlr first (SupportedFormats, SupportedPriorities and DisplayMessages, the number of messages the station can store) so you only send what will be accepted.

Setting a message with an id that already exists replaces it (O06). GetDisplayMessages takes a requestId and optional filters (id, priority, state), returns Accepted or Unknown, and delivers the stored messages asynchronously in NotifyDisplayMessages. ClearDisplayMessage removes one by id.

Messages for one transaction

Setting transactionId binds a message to a running transaction (O02). It is shown only during that transaction and is no longer relevant once it ends, which makes it the right tool for per-session information such as "Charging limited to 11 kW by site load management" or a reminder about idle fees:

json
[2, "sdm-12", "SetDisplayMessage", {
  "message": {
    "id": 9001,
    "priority": "InFront",
    "state": "Charging",
    "transactionId": "7f3c2a10-5e61-4b8e-9d2a-0c4b1e8f6a33",
    "message": {
      "format": "UTF8",
      "language": "en",
      "content": "Idle fee starts 30 min after charging completes."
    }
  }
}]

[3, "sdm-12", {
  "status": "Accepted"
}]

If the transaction has already ended or is unknown to the station, the response is UnknownTransaction. Allocate transaction-bound message ids from a separate range so they never overwrite your site-wide messages by accident.

Languages

IdTokenInfo carries language1 and optionally language2, the driver's preferred languages as language codes. When a message exists in several languages, the station can choose the one matching the driver. In practice this means sending one SetDisplayMessage per language (each with its own id) for general messages, and returning personalMessage in the driver's language for personal ones.

Tariff and cost (block I)

In 2.0.1 the CSMS calculates cost; the station only displays what it is told. (Local cost calculation on the station is an OCPP 2.1 feature, covered in part 11.) The pieces fit together like this:

WhenMechanismUse case
Driver authorizesidTokenInfo.personalMessage in AuthorizeResponse or TransactionEventResponse carries the driver-specific tariff textI01
During chargingCostUpdated from CSMS, or totalCost in a TransactionEventResponseI02
After chargingtotalCost in the response to the Ended TransactionEventI03
CSMS unreachableTariffCostCtrlr.TariffFallbackMessage and TotalCostFallbackMessageI04, I05
Tariff changes mid-sessionupdatedPersonalMessage in TransactionEventResponseBlock I

Showing the tariff at authorization

json
[3, "au-0202", {
  "idTokenInfo": {
    "status": "Accepted",
    "language1": "en",
    "personalMessage": {
      "format": "UTF8",
      "language": "en",
      "content": "Your rate: 0.39 EUR/kWh. Idle fee after 30 min."
    }
  }
}]

Because the CSMS knows who the driver is, this is where contract-specific or roaming prices are shown. For roaming drivers, the text should reflect the tariff from your OCPI tariffs for that partner.

Running cost

As meter values arrive, the CSMS recomputes the cost and pushes it with CostUpdated, in the currency configured in TariffCostCtrlr.Currency:

json
[2, "cu-0451", "CostUpdated", {
  "totalCost": 7.42,
  "transactionId": "7f3c2a10-5e61-4b8e-9d2a-0c4b1e8f6a33"
}]

[3, "cu-0451", {}]

Alternatively, return totalCost in the response to periodic TransactionEvent updates, which avoids an extra round trip. Either way, rate-limit it: once a minute or on meaningful change is plenty, and a cost push on every meter value just adds load and log noise.

Final cost

json
[3, "te-0009", {
  "totalCost": 15.37,
  "idTokenInfo": {
    "status": "Accepted"
  }
}]

This is the response to the Ended event from part 4. It has a catch: you must answer quickly, so totalCost must be computable from data you already have. If billing involves an asynchronous step (a roaming partner's tariff, a payment capture), return your best estimate or leave totalCost out and let the station show TotalCostFallbackMessage. Never delay the response waiting for an external system.

Field gotchas

  • Rounding. totalCost is a decimal including taxes as you define them. Round the same way your invoice does, or drivers will see a different number on the screen than on their receipt.
  • Offline transactions. Events delivered late (offline: true) produce cost responses the driver will never see. Do not rely on the screen as the only receipt; send it via app or email too.
  • Fallback text. Set TariffFallbackMessage and TotalCostFallbackMessage on every station during provisioning. A blank screen is worse than "Price shown in app".
  • Feature flags. Tariff and cost support is optional and is enabled per station through TariffCostCtrlr. Check before relying on it.

Next

Next: migrating from OCPP 1.6 to 2.0.1. For tariff design itself, see EV charging pricing models.

Let's Talk

Ready to build something that lasts?

Whether it's a charging network, an AI rollout, or a core enterprise system — let's talk about what you're building.