PermaLink Ein OCPP-Slot, mehrere Backends: warum ich ocpp-fanout gebaut habe09/10/2026 06:00 PM

Die meisten Wallboxen haben genau einen OCPP-WebSocket. Lokal soll trotzdem der eigene Regler Strom und Phasen setzen — MQTT, HTTP, Modbus, nicht das Backend in der Cloud. Gleichzeitig sollen Dienste wie ein Enphase IQ Energy Router, EverHome oder Monta live mitlesen. Wer den einen Slot an ein Vendor-CSMS hängt, gibt die Steuerung oft stillschweigend ab.

Dieser Beitrag beschreibt die Problemstellung und wie ein eigenes Open-Source-Projekt sie löst: ocpp-fanout.

Ausgangslage

OCPP 1.6-J ist der Kanal, den die Wallbox für die Backend-Kommunikation hat. Ein Slot heißt: eine WebSocket-URL. Alles, was die Box als CSMS ansieht, darf CallResults liefern — und in der Praxis auch Befehle schicken: SetChargingProfile, RemoteStart, ChangeConfiguration.

Ich will drei Dinge gleichzeitig:

  1. Laden, Phasen, Start/Stop durch den lokalen Regler
  2. Die Box bleibt OCPP-online
  3. Enlighten, EverHome und Monta sehen Live-Daten, ohne die Box zu übernehmen

Das widerspricht sich, sobald das „Monitor“-Backend ein volles CSMS ist.

Das Problem

Viele Monitor-Backends sind keine reinen Zuschauer. Sie schicken Calls zur Wallbox. Ohne CallResult gilt die Box in der App als tot. Mit unkontrollierter Weiterleitung übernimmt das Backend die Ampere. Beides ist falsch, wenn der lokale Regler die Ladeleistung setzt.

Konkret:

  • Enphase IQ Energy Router sendet laufend GetConfiguration und TriggerMessage(StatusNotification). Ohne lokale Antwort bleibt Enlighten leer, obwohl der WebSocket steht. Leitet man SetChargingProfile durch, springen die Ampere — der Router soll im Monitor-Modus zuschauen, nicht laden.
  • EverHome spricht WebSocket ohne Subprotokoll ocpp1.6. Ein Standard-OCPP-Proxy bricht mit Server sent no subprotocol ab.
  • Monta braucht CallResults, optional RemoteStart und stimmige StartTransaction / transactionId. Ein Dummy, der andere IDs vergibt, füllt das Ladeprotokoll nicht.

Die Wallbox kann das nicht gleichzeitig erfüllen. Es gibt nur einen Slot.

Ein Splitter allein reicht nicht

joulo-ocpp-proxy macht genau den Split 1→N: eine eingehende Verbindung, ein Primary mit voller Kontrolle, beliebig viele Secondaries als Spiegel. Secondary zur Wallbox wird verworfen. Das ist gewollt: ein Secondary darf die Box nicht fernsteuern.

RichtungPrimarySecondary
Wallbox → CSMSweitergeleitetgespiegelt
CSMS → Wallboxweitergeleitetverworfen

Genau dort kippt der Plan. Die Monitor-Backends schicken trotzdem Calls. Joulo wirft sie weg und antwortet nicht an ihrer Stelle. Es gibt kein CallResult, die Vendor-App bleibt ohne Live-Status (historische kWh können trotzdem erscheinen).

joulo-ocpp-proxy allein: Enphase und EverHome bleiben dunkel

Joulo hat außerdem nur einen Schalter SECONDARY_CSMS_APPEND_CHARGE_POINT_ID. Enphase will die Charge-Point-ID in der URL, EverHome und Monta haben die Identität oft schon im Pfad. Ein Flag für alle Secondaries passt nicht.

Die Lösung: ocpp-fanout

Ich habe Joulo nicht geforkt. Der Proxy bleibt das Image von ghcr.io. Davor und dahinter sitzt eigener Code: ein Dummy-Primary, kleine Antwort-Shims und eine Web-UI.

ocpp-fanout: lokaler Regler steuert, Shims halten die Secondaries am Leben

Drei Ergänzungen schließen die Lücken:

  1. dummy-csms als Primary. Die Box braucht irgendwohin einen Peer, der CallResults liefert ([3, id, payload]), sonst gilt sie als offline. Der Dummy tut das. Eigene Steuerung sendet er nicht. POST /inject ist der einzige Weg, über Joulo einen CSMS-Call zur Box zu schicken — und der Endpunkt hängt nur am Compose-Netz, nicht am Host.
  2. Shims zwischen Joulo und jedem knickrigen Secondary. Joulo sieht einen normalen OCPP-1.6-Peer. Der Shim spricht upstream so, wie der Vendor es wirklich tut. Standard: CSMS-Calls lokal beantworten, die Wallbox sieht sie nie. Allowlist in der Setup-UI: ausgewählte Calls werden auf der Primary-Leitung injiziert, Joulo leitet sie zur Box, CallResult kommt zurück zum Vendor.
  3. Web-UI auf demselben Host: Live-Paketfluss, Setup für Backend-URLs und Allowlist, Dark/Light. Erlaubte Befehle gelten sofort. Den Stack neu starten muss man nur nach URL-Änderung.

Ein Image, drei Shim-Dienste (SHIM_NAME / SHIM_PORT): Enphase, EverHome, Monta.

Wer darf was

ZielWer
Laden, Phasen, Start/Stoplokaler Regler (MQTT / Enphase HTTP, nicht OCPP)
Box bleibt OCPP-onlineDummy-CSMS als Primary
Enlighten / EverHome / Monta sehen Live-DatenSecondaries über Shims
Ausgewählte CSMS-Befehle dürfen zur BoxSetup-Allowlist → Dummy-Inject durch Joulo
Alles andere bleibt von der Box fernShim antwortet lokal, UI zeigt dropped

Die Wallbox zeigt auf einen WebSocket: ws://<host>:9100/<chargePointId>.

Was die Shims konkret tun

  • Enphase: lokale Antworten auf GetConfiguration und TriggerMessage. Wer Live-Status in Enlighten will, erlaubt in Setup TriggerMessage:StatusNotification. SetChargingProfile bleibt von der Allowlist fern, solange der Router nur beobachten soll.
  • EverHome: WebSocket ohne Subprotokoll ocpp1.6, sonst kommt die Verbindung nicht zustande.
  • Monta: CallResults, optionales RemoteStart (nur wenn allowlisted), transactionId auf MeterValues / StopTransaction auf die von Monta vergebene ID mappen, StartTransaction nachspielen, wenn schon eine Session offen ist.

Spec-konforme lokale Antworten gibt es auch für RFID-Liste und Clear Profile (Accepted), ohne die Box zu beschreiben.

Sichtbarkeit

Ohne UI rät man, welcher Call wo landet. Die Live-Ansicht zeigt den Paketfluss: Wallbox, Proxy, Dummy, Shims, Backends — und den Korb dropped für alles, was der Secondary nicht zur Box durchreicht.

Live-Ansicht: Gemini, EV, joulo, dummy, Shims, Enphase, EcoTracker, Monta

Beim Laden zeigt die EV-Karte Phasen, Ampere je Phase und kW aus den MeterValues (Current.Import L1/L2/L3 und Power.Active.Import).

Betrieb

Zwei Compose-Dateien: voller Stack mit Healthchecks und rotierten Logs, oder eine einfache Variante ohne Healthchecks, in der Charge-Point-ID, Backend-URLs und Allowlist nur in der Setup-UI stehen. Compose baut Dummy, Shim und UI lokal; Joulo kommt von ghcr.io.

git clone https://github.com/cbrandlehner/ocpp-fanout.git
cd ocpp-fanout
git checkout v1.0.0
cp .env.example .env
docker compose up -d --build

Web-UI: http://<host>:8088/. Den OCPP-WebSocket (Standardport 9100) und die Setup-UI nicht ins öffentliche Internet legen. Strom und Phasen gehören zum lokalen Regler, nicht zum Secondary, es sei denn, ein Befehl steht bewusst auf der Allowlist.

Quellcode und Release 1.0: github.com/cbrandlehner/ocpp-fanout, Tag v1.0.0. Deutsche Bedienung: docs/ANLEITUNG.md.

Hinweis

ocpp-fanout ist ein unabhängiges Open-Source-Projekt ohne Herstellersupport. Es steht in keiner Verbindung zu go-e, Enphase, everHome, Monta, Joulo, Tesla oder anderen Anbietern, deren Produkte es ansprechen kann, und wird von diesen weder unterstützt noch empfohlen. Nutzung auf eigene Gefahr. Produktnamen dienen nur der Beschreibung der Anbindung.

Fazit

Ein OCPP-Slot kann nicht gleichzeitig lokaler Regler und drei Vendor-CSMS sein. Joulo teilt 1→N und hält Secondary-Steuerung von der Box fern — das allein lässt Enlighten, EverHome und Monta aber tot aussehen, weil ihnen die CallResults fehlen. ocpp-fanout ergänzt Dummy, Shims und UI: die Secondaries bleiben am Leben, Antworten sind lokal, und nur eine explizite Allowlist darf einen Befehl zur Wallbox injizieren. Die Steuerung bleibt, wo sie hingehört.


2 .
Hinweis
Das Weblog gibt meine persönlichen Ansichten und Kommentare wieder und entspricht nicht den Positionen meiner aktuellen oder früheren Arbeitgeber oder Kunden.
Über mich
Nach Kategorie
Meine Seiten
netcraft Linux host Blog Admin OpenNTF
Monatsarchiv