Hi @splatch and @PRosenb,
Following up on https://community.openhab.org/t/charging-openhab-with-ocpp/135114, I've been running an eu.chargetime.ocpp:ocpp16j:2.0-based OCPP central system against two Wallbox chargers in production and would like to contribute back. This is a single tracking issue for what I found; I'd open the individual PRs against master after we agree on scope and cadence.
Environment
- Two Wallbox chargers, both firmware 6.7.38:
- Wallbox Copper SB (
CPB1-S-2-4, socketed Type 2)
- Wallbox Pulsar Plus (
PLP1-0-2-4)
- OCPP 1.6-J central system bound to
0.0.0.0:8887
- Both chargers also reachable over
wallbox-mqtt-bridge for ground-truth comparison
- openHAB 5.x on Debian, Java 21
All findings below are empirically reproduced on those units. Happy to share full session captures (frames + logs) if useful.
Findings
1. WebSocket 1006 idle reconnect cycle — PING_INTERVAL mismatch
Out of the box the chargers cycled the WebSocket with close code 1006 (remote=true) every 30–45 s. Root cause is in OcppServer.java:
this.server = new JSONServer(new ServerCoreProfile(handler));
The single-arg JSONServer constructor uses JSONConfiguration.get() defaults, which set PING_INTERVAL_PARAMETER = 60. That's passed straight through to Java-WebSocket's setConnectionLostTimeout(60). Wallbox's internal idle timeout fires before our PING does. Setting PING_INTERVAL_PARAMETER = 20 and passing a JSONConfiguration to the 2-arg constructor makes the connection stay up for hours / indefinitely.
2. Wallbox drops the socket if it receives an outbound CALL before its BootNotification is acknowledged
Pete's #122 ("register in newSession() since BootNotification isn't always sent") fixes the inbound side — registration happens early and outbound send() resolves to a UUID. But the binding still happily sends outbound CALLs immediately on newSession. Wallbox firmware specifically closes the socket if it receives any CALL before its boot handshake has completed for that session. See lbbrhzn/ocpp#1510 — confirmed on FW 6.7.38.
What works: gate outbound CALLs until either (a) handleBootNotificationRequest has fired this session, or (b) the session has been stable for N seconds with ≥1 StatusNotification (trust fallback for the case Wallbox reuses an existing boot state on reconnect).
3. OcppMeasurementMapping.get() returns null for Wallbox's measurand names
Wallbox firmware emits the phase inside the measurand string, with an empty phase field on SampledValue. Live GetConfiguration output from FW 6.7.38 (excerpt):
MeterValuesAlignedData = Energy.Active.Import.Register,Power.Active.Import,
Current.Import.L1,Current.Import.L2,Current.Import.L3,
Voltage.L1-N,Voltage.L2-N,Voltage.L3-N,
Current.Offered,Power.Offered,Temperature
OcppMeasurementMapping.MAPPING.get("Current.Import.L1") returns null → silently dropped in ConnectorThingHandler.handleMeterValues(). Fix: strip a trailing .L1/.L2/.L3/.L1-N from the measurand before lookup, then route to per-phase channels.
4. Persisted hardware max current on Wallbox is the vendor key chargingALimitConn1, not OCPP-standard MaxCurrent
ConnectorThingHandler.handleCommand() sends ChangeConfiguration[MaxCurrent=N] for the currentLimit channel. Wallbox firmware silently ignores MaxCurrent. ChangeConfiguration[chargingALimitConn1=N] is Accepted and the value mirrors instantly on the Wallbox MQTT bridge's max_charging_current topic — same underlying register. Suggested: a hardwareMaxCurrentKey thing parameter (default MaxCurrent to keep current behavior; chargingALimitConn1 for the Wallbox vendor preset).
5. SmartCharging and RemoteTrigger feature profiles aren't registered on JSONServer
new JSONServer(new ServerCoreProfile(handler)) only enables Core. SetChargingProfile, ClearChargingProfile, TriggerMessage then throw UnsupportedFeatureException on server.send(...). Adding server.addFeatureProfile(new ServerSmartChargingProfile()) and ... new ServerRemoteTriggerProfile() unblocks:
- dynamic current limit via
TxDefaultProfile (the only chargingProfilePurpose Wallbox firmware accepts)
- pause via
SetChargingProfile(limit=0) and resume via SetChargingProfile(limit=N)
TriggerMessage(MeterValues) as a watchdog for stale telemetry
6. ClockAlignedDataInterval = 0 by default → no MeterValues outside a transaction
Without auto-config, telemetry only flows during an active charge session. Sending ChangeConfiguration[ClockAlignedDataInterval=30] plus setting MeterValuesAlignedData to the same measurand list makes the charger emit per-phase voltage/temperature even when idle. Worth doing once per session, post-boot.
7. Outbound CompletableFuture from Server.send() is never completed when the socket dies
ChargeTimeEU/Java-OCA-OCPP#121. The PromiseRepository retains the future indefinitely; on socket close it is silently orphaned. Wrapping every server.send(sid, req).toCompletableFuture().orTimeout(15, SECONDS) surfaces the failure as a TimeoutException in the log instead of silence.
Proposed PRs
Small, focused, easy to review independently:
- Configurable
pingInterval on the server bridge (default 60 to match current behavior, can be lowered for Wallbox)
- Register
ServerSmartChargingProfile + ServerRemoteTriggerProfile on the JSONServer
- MeterValues parser: strip phase suffix from measurand; route to per-phase channels
.orTimeout() wrapper on outbound CALLs (and surface timeouts cleanly in the log)
- Boot-gate / trust-fallback for outbound CALLs (the big one — happy to discuss design first)
- New channels:
pause, cableConnected (derived from StatusNotification), reset (Soft/Hard)
hardwareMaxCurrentKey thing parameter + hardwareMaxCurrent channel (defaults to MaxCurrent for backwards compatibility)
- Auto-config
ClockAlignedDataInterval + MeterValuesAlignedData post-boot
- One-shot
GetConfiguration diagnostic dump on session-ready (log every key=value)
- Optional:
chargerVendor dropdown (generic/wallbox/...) that presets sensible defaults for several of the above
PRs 1, 2, 4, 9 are tiny and low-risk. PR 5 is the meatiest and worth a design discussion before code.
Questions
- Do you have a preferred PR cadence — one tracking discussion thread + 10 small PRs, or batched into 2-3 larger ones?
- Are you open to adding a
chargerVendor preset dropdown (PR 10), or would you prefer to keep the binding strictly OCPP-spec-level with vendor presets as a separate downstream binding?
- Anything in the above already covered by work-in-progress on your side that I should know about before opening PRs?
Thanks for the binding — it's the only OCPP option in the openHAB ecosystem and a solid foundation.
— Stamate Viorel (@stamateviorel)
Disclosure: this analysis and the proposed fixes were developed and tested over a production deployment with assistance from Anthropic's Claude (Claude Code). The findings are reproduced empirically; the Claude-assisted parts are the writeup and code drafts that follow.
Hi @splatch and @PRosenb,
Following up on https://community.openhab.org/t/charging-openhab-with-ocpp/135114, I've been running an
eu.chargetime.ocpp:ocpp16j:2.0-based OCPP central system against two Wallbox chargers in production and would like to contribute back. This is a single tracking issue for what I found; I'd open the individual PRs againstmasterafter we agree on scope and cadence.Environment
CPB1-S-2-4, socketed Type 2)PLP1-0-2-4)0.0.0.0:8887wallbox-mqtt-bridgefor ground-truth comparisonAll findings below are empirically reproduced on those units. Happy to share full session captures (frames + logs) if useful.
Findings
1. WebSocket 1006 idle reconnect cycle —
PING_INTERVALmismatchOut of the box the chargers cycled the WebSocket with
close code 1006 (remote=true)every 30–45 s. Root cause is inOcppServer.java:The single-arg
JSONServerconstructor usesJSONConfiguration.get()defaults, which setPING_INTERVAL_PARAMETER = 60. That's passed straight through to Java-WebSocket'ssetConnectionLostTimeout(60). Wallbox's internal idle timeout fires before our PING does. SettingPING_INTERVAL_PARAMETER = 20and passing aJSONConfigurationto the 2-arg constructor makes the connection stay up for hours / indefinitely.2. Wallbox drops the socket if it receives an outbound CALL before its BootNotification is acknowledged
Pete's #122 ("register in newSession() since BootNotification isn't always sent") fixes the inbound side — registration happens early and outbound
send()resolves to aUUID. But the binding still happily sends outbound CALLs immediately onnewSession. Wallbox firmware specifically closes the socket if it receives any CALL before its boot handshake has completed for that session. See lbbrhzn/ocpp#1510 — confirmed on FW 6.7.38.What works: gate outbound CALLs until either (a)
handleBootNotificationRequesthas fired this session, or (b) the session has been stable for N seconds with ≥1 StatusNotification (trust fallback for the case Wallbox reuses an existing boot state on reconnect).3.
OcppMeasurementMapping.get()returnsnullfor Wallbox's measurand namesWallbox firmware emits the phase inside the measurand string, with an empty
phasefield onSampledValue. LiveGetConfigurationoutput from FW 6.7.38 (excerpt):OcppMeasurementMapping.MAPPING.get("Current.Import.L1")returns null → silently dropped inConnectorThingHandler.handleMeterValues(). Fix: strip a trailing.L1/.L2/.L3/.L1-Nfrom the measurand before lookup, then route to per-phase channels.4. Persisted hardware max current on Wallbox is the vendor key
chargingALimitConn1, not OCPP-standardMaxCurrentConnectorThingHandler.handleCommand()sendsChangeConfiguration[MaxCurrent=N]for thecurrentLimitchannel. Wallbox firmware silently ignoresMaxCurrent.ChangeConfiguration[chargingALimitConn1=N]isAcceptedand the value mirrors instantly on the Wallbox MQTT bridge'smax_charging_currenttopic — same underlying register. Suggested: ahardwareMaxCurrentKeything parameter (defaultMaxCurrentto keep current behavior;chargingALimitConn1for the Wallbox vendor preset).5.
SmartChargingandRemoteTriggerfeature profiles aren't registered onJSONServernew JSONServer(new ServerCoreProfile(handler))only enables Core.SetChargingProfile,ClearChargingProfile,TriggerMessagethen throwUnsupportedFeatureExceptiononserver.send(...). Addingserver.addFeatureProfile(new ServerSmartChargingProfile())and... new ServerRemoteTriggerProfile()unblocks:TxDefaultProfile(the onlychargingProfilePurposeWallbox firmware accepts)SetChargingProfile(limit=0)and resume viaSetChargingProfile(limit=N)TriggerMessage(MeterValues)as a watchdog for stale telemetry6.
ClockAlignedDataInterval = 0by default → no MeterValues outside a transactionWithout auto-config, telemetry only flows during an active charge session. Sending
ChangeConfiguration[ClockAlignedDataInterval=30]plus settingMeterValuesAlignedDatato the same measurand list makes the charger emit per-phase voltage/temperature even when idle. Worth doing once per session, post-boot.7. Outbound
CompletableFuturefromServer.send()is never completed when the socket diesChargeTimeEU/Java-OCA-OCPP#121. The
PromiseRepositoryretains the future indefinitely; on socket close it is silently orphaned. Wrapping everyserver.send(sid, req).toCompletableFuture().orTimeout(15, SECONDS)surfaces the failure as aTimeoutExceptionin the log instead of silence.Proposed PRs
Small, focused, easy to review independently:
pingIntervalon the server bridge (default 60 to match current behavior, can be lowered for Wallbox)ServerSmartChargingProfile+ServerRemoteTriggerProfileon theJSONServer.orTimeout()wrapper on outbound CALLs (and surface timeouts cleanly in the log)pause,cableConnected(derived fromStatusNotification),reset(Soft/Hard)hardwareMaxCurrentKeything parameter +hardwareMaxCurrentchannel (defaults toMaxCurrentfor backwards compatibility)ClockAlignedDataInterval+MeterValuesAlignedDatapost-bootGetConfigurationdiagnostic dump on session-ready (log every key=value)chargerVendordropdown (generic/wallbox/...) that presets sensible defaults for several of the abovePRs 1, 2, 4, 9 are tiny and low-risk. PR 5 is the meatiest and worth a design discussion before code.
Questions
chargerVendorpreset dropdown (PR 10), or would you prefer to keep the binding strictly OCPP-spec-level with vendor presets as a separate downstream binding?Thanks for the binding — it's the only OCPP option in the openHAB ecosystem and a solid foundation.
— Stamate Viorel (@stamateviorel)
Disclosure: this analysis and the proposed fixes were developed and tested over a production deployment with assistance from Anthropic's Claude (Claude Code). The findings are reproduced empirically; the Claude-assisted parts are the writeup and code drafts that follow.