# OCPP Binding
This binding lets openHAB act as an OCPP 1.6-J central system, so EV chargers (charge points) that speak OCPP connect directly to openHAB — no vendor cloud required. It is built on the ChargeTime OCA-OCPP (opens new window) library.
Chargers open a WebSocket connection to openHAB and are modelled as a three-tier hierarchy that mirrors OCPP itself: one server endpoint, the charge points that dial in to it, and the connectors of each charge point.
It reports connection state, connector status and metering, and controls charging: current limit, pause, remote start/stop, availability, unlock and reset.
# Supported Things
server: the OCPP JSON WebSocket endpoint chargers connect to, and the bridge for all charge points.chargepoint: one physical charger matched to a session by its OCPP charge point id (the URL path it dials, without the leading slash), and the bridge for its connectors.connector: one connector (outlet) of a charger, carrying the live status and metering channels.
# Discovery
Discovery is passive — chargers announce themselves.
When a charger connects with a charge point id that has no chargepoint thing, it appears in the inbox under its server bridge.
When a known charge point reports a connector for the first time, that connector appears in the inbox under its chargepoint bridge.
There is no active scan; point your charger at ws://<openhab-host>:<port>/<chargePointId> and it will show up.
You do not need to know the charge point id in advance: connect the charger and it appears in the inbox under its real id, ready to accept.
Only if it never shows up, set the binding's log level to DEBUG (see Logging) and look for the Charger connected: id=... line — it prints the exact id the charger dialed, which helps track down a URL or connection problem.
The id is whatever path the charger appends to its backend URL — often its serial number — so it is easiest to read it back here rather than hunt for it in the charger's own settings.
# Thing Configuration
# server
| Name | Type | Description | Default | Required | Advanced |
|---|---|---|---|---|---|
| port | integer | TCP port the OCPP server listens on | 8887 | no | no |
| host | text | Local bind address | 0.0.0.0 | no | yes |
| heartbeatInterval | integer | Heartbeat interval (s) returned to chargers on boot | 300 | no | yes |
| meterValuesData | text | Measurands to configure on chargers (empty = leave unchanged) | (empty) | no | yes |
| meterValueSampleInterval | integer | MeterValueSampleInterval to configure (-1 = leave unchanged) | -1 | no | yes |
| clockAlignedDataInterval | integer | ClockAlignedDataInterval to configure (-1 = leave unchanged) | -1 | no | yes |
| disableRemoteTxAuthorization | boolean | Configure AuthorizeRemoteTxRequests=false | false | no | yes |
| extraConfig | text[] | Extra ChangeConfiguration entries as key=value, applied on boot | (empty) | no | yes |
| pingInterval | integer | WebSocket ping interval (s). A charger that does not answer a ping is disconnected, and many never do — leave at 0 unless yours is known to reply | 0 | no | yes |
| requestTimeoutSeconds | integer | Seconds before an unanswered request to a charger fails | 30 | no | yes |
| authPassword | text | HTTP Basic password chargers must present (username = charge point id), 16–20 visible ASCII characters. Empty disables authentication | (empty) | no | yes |
| whitelistTagIds | text[] | idTag whitelist. Empty accepts every tag; otherwise unknown tags are rejected | (empty) | no | yes |
| chargerIds | text[] | Charge point id allow-list. Empty accepts any charger; otherwise unlisted ones are rejected | (empty) | no | yes |
These settings are pushed to a charger as ChangeConfiguration requests after it boots — or after it reconnects without booting, which is what happens when openHAB restarts — one at a time, and only until the charger has accepted them once for the configured values.
A changed configuration is sent again on the charger's next boot or reconnect; an unchanged one is not repeated.
After three failed attempts the burst is not retried until the configuration changes or openHAB restarts.
A request a charger leaves unanswered fails after requestTimeoutSeconds; the OCPP library itself would wait on it forever.
Measurands a charger rejects are dropped one at a time until it accepts them, and the accepted set is remembered per configuration key.
The binding also runs a liveness watchdog sized from the heartbeat interval — any answer from the charger counts as life — and self-heals when a charger reconnects under a new session.
For the charger's own offline authorization cache, see the chargepoint local-auth-list and learn-card channels below.
# chargepoint
| Name | Type | Description | Default | Required | Advanced |
|---|---|---|---|---|---|
| chargePointId | text | The charger's OCPP identity (its WebSocket URL suffix) | N/A | yes | no |
| configSettleSeconds | integer | Delay after BootNotification before the configuration above is sent. Some chargers are not ready to answer immediately | 0 | no | yes |
| meterless | boolean | The charger has no internal meter: send only ClockAlignedDataInterval=0 and ignore meterValueSampleInterval, meterValuesData and clockAlignedDataInterval for it | false | no | yes |
| heartbeat | integer | Per-charger heartbeat interval (s), overriding the server default. Also sizes this charger's liveness window. 0 uses the server default | 0 | no | yes |
# connector
| Name | Type | Description | Default | Required | Advanced |
|---|---|---|---|---|---|
| connectorId | integer | OCPP connector number (1..N) | 1 | no | no |
| forceTxDefaultProfile | boolean | Always send the charge limit as a TxDefaultProfile, even during a transaction. Needed for chargers that reject a TxProfile outside one | false | no | yes |
| profileMinIntervalMs | integer | Minimum spacing (ms) between SetChargingProfile sends; rapid changes are coalesced. 0 disables | 0 | no | yes |
| hardwareMaxCurrentKey | text | Vendor ChangeConfiguration key backing the hardware-max-current channel. Empty disables that channel | (empty) | no | yes |
| remoteStartTag | text | idTag used when starting a transaction via the charging channel | openhab | no | yes |
| refreshInterval | integer | Poll this connector for MeterValues every N seconds via TriggerMessage. 0 disables polling | 0 | no | yes |
| nominalVoltage | decimal | Line voltage for converting an amps charge-limit to watts on a charger that only accepts a power limit (W = A×V×phases) | 230 | no | yes |
| phases | integer | Phases assumed in that amps→watts conversion — 1 single-phase, 3 three-phase | 1 | no | yes |
| stuckStateRecovery | boolean | Send an UnlockConnector if the connector stays in a transient state (Preparing/Finishing) for more than 120 s. Off by default; enable only for a charger known to wedge there | false | no | yes |
| remoteStartRetries | integer | Retry a RemoteStart the charger does not answer, this many times. 0 disables. For a charger that drops the first start request but accepts a retry | 0 | no | yes |
Most connectors need no configuration beyond connectorId.
The rest cover specific charger behaviors.
forceTxDefaultProfile is for chargers that reject a TxProfile when no transaction is active — a Phoenix CHARX does: the charge limit is then sent as a TxDefaultProfile, which such chargers accept and apply through their own load management.
profileMinIntervalMs coalesces rapid limit changes into at most one SetChargingProfile per interval, which keeps a solar-tracking rule that adjusts the limit every few seconds from flooding the charger.
refreshInterval actively polls a connector for MeterValues for chargers that do not push them on their own; a poll is skipped while the previous one is still outstanding, so a charger that stops answering cannot build a backlog.
hardwareMaxCurrentKey binds the hardware-max-current channel to a vendor ChangeConfiguration key, since the hardware ceiling is not a standard OCPP setting; the channel appears only once that key is set.
stuckStateRecovery is left off because auto-unlocking a connector is a physical side effect, and Preparing and Finishing are normal states a charger can dwell in.
remoteStartRetries is for a charger that intermittently ignores the first RemoteStartTransaction: the binding re-sends it up to that many times, a few seconds apart, and stops as soon as a transaction starts, so it never double-starts.
Off (0) by default, so a charger that answers first time is unaffected.
# Channels
# chargepoint
| Channel | Type | Read/Write | Description |
|---|---|---|---|
| connected | Switch | R | Whether the charger has an open session |
| last-seen | DateTime | R | Timestamp of the last contact from the charger |
| reset | Switch | W | Momentary — soft reset the charge point |
| local-auth-list | String | RW | The charger's local authorization list (comma-separated idTags), persisted — set it to push cards for offline use |
| learn-card | Switch | W | Momentary — switch ON, then present a card at the reader and it is added to the local authorization list |
Vendor, model, firmware version and serial number are published as thing properties from the charger's BootNotification.
The local authorization list lets a cached RFID card start a charge while openHAB or the network is offline, on a charger that supports LocalAuthListManagement.
The list lives on the local-auth-list channel as a comma-separated set of idTags: set it (from a rule or the UI) and the binding pushes it to the charger with SendLocalList.
It is compared with the charger's list version on every connect and sent only when they differ, so a change made while the charger was offline, clearing it included, reaches it when it reconnects.
An empty list only goes to a charger this binding has given a list before, so a charger that keeps its own cards is left alone.
It is persisted on the charge point thing, so it survives an openHAB restart.
To add a card without knowing its id, use learn-card: switch it ON and present the card at the reader — the binding takes the idTag from the charger's Authorize and adds it to the list, then disarms.
Learning also disarms on its own after 60 s if no card is presented.
A charger that does not advertise the profile is left untouched.
# connector
| Channel | Type | Read/Write | Description |
|---|---|---|---|
| charge-point-status | String | R | OCPP status (Available, Preparing, Charging, ...) |
| cable-connected | Switch | R | Whether a vehicle cable is plugged in (derived) |
| current-import-l1/l2/l3 | Number:ElectricCurrent | R | Imported current per phase (MeterValues) |
| voltage-l1/l2/l3 | Number:ElectricPotential | R | Voltage per phase (MeterValues) |
| current-offered | Number:ElectricCurrent | R | Current offered to the vehicle |
| power-active-import | Number:Power | R | Active power imported |
| power-offered | Number:Power | R | Power offered to the vehicle |
| energy-active-import | Number:Energy | R | Energy register (Energy.Active.Import.Register) |
| session-energy | Number:Energy | R | Energy of the last session (meter-stop − meter-start), published once at session end |
| charging | Switch | RW | ON while a transaction runs; command to remote start/stop |
| charge-limit | Number:ElectricCurrent | RW | Charge current cap via SetChargingProfile; 0 stops the charge |
| power-limit | Number:Power | RW | Charge power cap (watts) for power-only chargers; takes over from charge-limit until a later charge-limit clears it |
| number-phases | Number | RW | Phases to charge on (1/2/3); 0 = charger default. Needs a charger that supports phase switching |
| pause | Switch | RW | Pause charging (profile limit 0) without ending the transaction |
| clear-limit | Switch | W | Momentary — remove charge-limit and power-limit, so the charger returns to its own maximum |
| availability | Switch | RW | OCPP availability (Operative/Inoperative) |
| unlock | Switch | W | Momentary — unlock the connector |
| hardware-max-current | Number:ElectricCurrent | RW | Hardware current ceiling via a vendor config key; only created when hardwareMaxCurrentKey is set |
Beyond the channels above, the rest of the OCPP 1.6 SampledValue set is added to the connector the first time the charger reports it: current-import, current-export, voltage, frequency, power-active-export, power-reactive-import, power-reactive-export, power-factor, energy-active-export, energy-active-import-interval, energy-active-export-interval, energy-reactive-import, energy-reactive-export, energy-reactive-import-interval, energy-reactive-export-interval, soc, rpm and temperature.
Every connector also carries the transaction metadata (id-tag, transaction-id, meter-start, meter-stop) and the metering timestamps (timestamp, timestamp-start, timestamp-stop).
A transaction that was open when openHAB restarted keeps its transaction-id and its meter-start, so session-energy is still sized when it stops.
At a stop, meter-stop and session-energy are published before transaction-id is cleared, so a rule triggered by transaction-id changing to UNDEF reads the settled total.
For chargers that reject a TxProfile outside a transaction (e.g. Phoenix CHARX), set forceTxDefaultProfile on the connector so the charge limit is sent as a TxDefaultProfile.
# Controlling a Charge
The connector's writable channels map to OCPP commands, and each updates only once the charger confirms the command — a rejected request leaves the channel showing the real state rather than the requested one.
charging starts and stops a transaction: sending it ON issues a RemoteStartTransaction, OFF a RemoteStopTransaction.
The transaction is started with the idTag from the connector's remoteStartTag (default openhab), which has to be authorized: by this binding through the server thing's whitelistTagIds list (empty accepts any tag), and by the charger itself if it enforces its own whitelist.
So if ON does nothing, set remoteStartTag to a tag your charger accepts, or allow that tag on the charger.
Most chargers also only start once a vehicle is plugged in, so a RemoteStart on an idle connector is often ignored.
Because charging follows the charger's reported status, it also reads ON on its own whenever a transaction is running, however it was started.
Stopping needs the transaction id the charger assigned when the session began, which openHAB records from every StartTransaction it receives — a session started by the vehicle, a card or the charger's own app can be stopped from charging like one started here.
The one session it cannot stop is one that began while openHAB was down or before the connector thing existed, since that StartTransaction was never received: OFF is then logged and does nothing.
For such a session, suspend the power with pause (a 0 A profile needs no transaction) or end it with the chargepoint-level reset (which reboots the whole charger).
charge-limit caps the charging current: the value is sent as a SetChargingProfile and the channel reflects the applied limit once accepted.
Setting it to 0 stops the charge, as a 0 A limit does in OCPP, and clear-limit removes the cap, so the charger goes back to its own maximum.
Unless forceTxDefaultProfile is set, the limit goes out during a transaction as a TxProfile, which the charger drops when that transaction ends, and outside one as the TxDefaultProfile the next transaction starts under.
So when a transaction that got a TxProfile ends, the binding sends the current limit again as the TxDefaultProfile, and the next session starts at the last limit, or paused if it was paused.
Some chargers only accept a charge limit expressed in watts (their OCPP ChargingScheduleAllowedChargingRateUnit is Power, not Current); the binding learns this from the charger and converts charge-limit amps to watts with nominalVoltage and phases, so the same amps channel still works.
Alternatively set power-limit (watts) directly — it is sent as-is, with no conversion, on any charger that accepts a power limit, and takes over from charge-limit while it is set.
Commanding charge-limit again clears the power-limit and returns to amps, so the most recent command always wins.
A power-limit of 0 stops the charge too, and clear-limit removes it along with charge-limit.
number-phases requests charging on a given number of phases (1, 2 or 3) by setting numberPhases in the charging profile — for switching a car to single-phase when solar surplus is low, for instance; 0 clears the request so the charger keeps its own default (OCPP assumes 3).
It only takes effect on a charger that supports phase switching (its ConnectorSwitch3to1PhaseSupported is true), and when set it also drives the amps→watts conversion above.
pause suspends charging with a 0 A profile without ending the transaction; switching it off resumes — at your charge-limit if one is set, otherwise by removing the cap so the charger returns to its own maximum — distinct from charging, which ends the session.
A pause is a 0 A limit, so a resume must lift the cap rather than send another 0 A, which a charger reads as "stay suspended".
A charge-limit set during a pause is kept and sent when the pause ends.
availability takes the connector Operative or Inoperative, unlock releases the cable lock, and the chargepoint-level reset performs a soft reset of the whole charger.
# Full Example
# demo.things
Bridge ocpp:server:main [ port=8887 ] {
Bridge chargepoint wallbox "Wallbox" [ chargePointId="wallbox" ] {
Thing connector c1 "Connector 1" [ connectorId=1 ]
}
}
# demo.items
String Wallbox_Status "Status [%s]" { channel="ocpp:connector:main:wallbox:c1:charge-point-status" }
Switch Wallbox_Cable "Cable connected" { channel="ocpp:connector:main:wallbox:c1:cable-connected" }
Number:Power Wallbox_Power "Power [%.0f W]" { channel="ocpp:connector:main:wallbox:c1:power-active-import" }
Number:Energy Wallbox_Energy "Energy [%.2f kWh]" { channel="ocpp:connector:main:wallbox:c1:energy-active-import" }
Switch Wallbox_Charging "Charging" { channel="ocpp:connector:main:wallbox:c1:charging" }
Switch Wallbox_Pause "Pause" { channel="ocpp:connector:main:wallbox:c1:pause" }
Number:ElectricCurrent Wallbox_Limit "Limit [%.0f A]" { channel="ocpp:connector:main:wallbox:c1:charge-limit" }
Switch Wallbox_Reset "Reset charger" { channel="ocpp:chargepoint:main:wallbox:reset" }
Switch Wallbox_Force_Stop "Force stop"
The reset channel is on the chargepoint, not the connector, and is momentary — the binding pops it back OFF after sending.
Wallbox_Force_Stop is an unbound helper for the rule below.
# demo.rules
// Charge only overnight, using pause — which works even for a session openHAB did not start.
rule "Resume charging at 23:00"
when
Time cron "0 0 23 ? * *"
then
Wallbox_Pause.sendCommand(OFF)
end
rule "Pause charging at 07:00"
when
Time cron "0 0 7 ? * *"
then
Wallbox_Pause.sendCommand(ON)
end
// Follow solar surplus: cap the current to the spare power (single phase, ~230 V).
// A starting point only; real solar charging also wants hysteresis and a breaker-headroom check.
rule "Track solar surplus"
when
Item Solar_Surplus_W changed
then
var Number surplus = Solar_Surplus_W.state as Number
var int amps = (surplus.doubleValue / 230.0).intValue
if (amps < 6) amps = 6
if (amps > 16) amps = 16
Wallbox_Limit.sendCommand(amps)
end
// Stop a session that began while openHAB was down (no transaction id, so `charging` OFF cannot end it).
// Cut the power with pause, or end the session with a reset (which reboots the charger).
rule "Force stop"
when
Item Wallbox_Force_Stop received command ON
then
Wallbox_Pause.sendCommand(ON)
// Wallbox_Reset.sendCommand(ON) // uncomment to end the session instead of only pausing
end
# Troubleshooting
# A Charge Point Stays UNKNOWN, or Nothing Appears in the Inbox
The charge point id is the path of the WebSocket URL the charger dials, so the charger must connect to ws://<host>:<port>/<chargePointId>.
A charger pointed at the bare root (ws://<host>:<port>/, nothing after the slash) sends no id and is ignored, logging connected without a charge point id in its URL path.
Put the id in the charger's backend URL — many chargers keep the URL and the id in separate fields, but it still has to end up as the URL path after the leading slash — and make the chargepoint thing's chargePointId match it exactly.
If you are unsure what the charger actually sends, enable log:set DEBUG org.openhab.binding.ocpp and read it off the Charger connected: id=... line.
# A Connector Sits at SuspendedEVSE and Will Not Charge
SuspendedEVSE means the charge point itself is withholding energy — a charging-profile limit or an authorization result — unlike SuspendedEV, which is the vehicle not drawing (battery full, or charging scheduled in the car).
Check the connector is not left paused and that neither charge-limit nor power-limit is 0, which stops the charge: sending pause OFF resumes at your charge-limit, and clear-limit returns the charger to its own maximum.
Some chargers also suspend when a charge-limit is set before a transaction starts — with no transaction it goes out as a TxDefaultProfile, which such a charger accepts but then drops to SuspendedEVSE a few seconds in.
On those, start the charge first and set the limit once it is Charging: send charging ON (or plug in), wait for Charging, then set charge-limit.
Adjusting it mid-charge afterwards works normally.
# A Charger Never Connects and the Log Shows Nothing After Start-Up
If the server starts (OCPP JSON server listening) but no Charger connected line ever follows, the charger is not completing the WebSocket handshake, so nothing reaches the binding.
First rule out the network: from a device on the charger's own network segment (not just any machine), check the openHAB host and port are reachable — nc -zv <openhab-host> 8887 — and use the host's IP rather than a .lan name to rule out DNS.
A charger that always sends an HTTP Basic-auth header — some send their id with a very short or empty password on every connection, a V2C Trydan being one — is accepted when no authPassword is configured, so that is not a cause of a silent no-connect.
# Checking What the Charger Was Actually Sent
log:set TRACE eu.chargetime.ocpp.JSONCommunicator logs every OCPP message in both directions: Send a message: is what openHAB sent, Receive a message: what the charger sent, and a request and its answer carry the same id.
That is the evidence a charger vendor needs when a charger answers Accepted and then does not act on it.
It also logs every meter value and idTag, so switch it off again with log:set DEFAULT eu.chargetime.ocpp.JSONCommunicator.
# Power Readings Lag Behind the Charger
The power-active-import and per-phase metering channels only update when the charger sends a MeterValues sample, so between samples they look stale (the energy register keeps climbing because it is a running total).
To sample more often, lower the server's meterValueSampleInterval — the binding pushes it to the charger (10–15 s is plenty).
For a charger that will not honour that, set the connector's refreshInterval to poll it for a fresh MeterValues on a fixed cadence via TriggerMessage.
Do not push either below a few seconds on an older charger.
# Charger-Specific Notes
Every charger dials ws://<openhab-host>:<port>/<chargePointId>; the only real differences are how each vendor's UI presents the URL and the id, and a few per-charger quirks.
| Charger | Configure on the charger | Notes |
|---|---|---|
| Phoenix Contact CHARX SEC-3xxx | ws://<host>:8887/<id> | No internal meter: set meterless on the chargepoint and forceTxDefaultProfile on the connector. Metered externally. |
| Wallbox Copper SB / Pulsar Plus | ws://<host>:8887/<id> | Works with defaults. |
| Alfen Eve Single Pro | ws://<host>:8887/<id> (CSMS URL in the ACE Service Installer) | Its BootNotification model can exceed OCPP's 20-character limit; the binding accepts it rather than refusing the charger. |
| Mennekes Amtron (Bender controller) | Backend URL ws://<host>:8887/ plus ChargeBoxIdentity <id> in a separate field | The controller joins them into ws://<host>:8887/<id>. Do not copy the /OCPPJProxy/v16/ path from the Bender docs — that is only for their proxy backend. |
| V2C Trydan | ws://<host>:8887/<id> | Sends a short-password HTTP Basic-auth header on every connection; accepted (the binding relaxes the library's password-length check when no authPassword is set). |
| Easee | ws://<host>:8887/<id> | Answers Accepted to a 6 A limit but does not start charging on it; 7 A works. After a reboot one unit ignored every limit until its dynamic circuit limit was set again in the Easee app. |
# Security
Without authPassword the endpoint runs OCPP security profile 0: a plain-text WebSocket that accepts every connection, appropriate only on a trusted LAN.
Anyone who can reach the port can connect under any charge point id, so restrict exposure by binding a specific interface (host) or with firewall rules.
Setting authPassword enables HTTP Basic authentication (security profile 1): a charger must present the password with its charge point id as the username, and other connections are rejected before a session opens.
The authPassword must be 16–20 visible ASCII characters (the OCPP profile-1 rule). A charger that sends a Basic-auth header when no authPassword is set is accepted whatever its password length, so chargers that always send one still connect.
← Oceanic OJElectronics →