Dieses Projekt enthält ein diagnostisches Python-Skript (connect_vr921.py), das mit einem Vaillant VR921/EEBUS-Gateway über SHIP (TLS WebSocket) spricht und darüber SPINE-Datagramme austauscht.
Das Skript kann:
- ein Client-Zertifikat erzeugen und wiederverwenden (stabile Identität über SKI)
- sich per mDNS (
_ship._tcp.local.) selbst ankündigen und den VR921 finden - eine wss://…/ship/ Verbindung aufbauen und den SHIP-Handshake durchführen
- auf gateway-initiierte SPINE READ/CALL Nachrichten reagieren (wichtig für Interoperabilität)
- Measurement-Server entdecken, abonnieren und Messwerte lesen
- optional Messwerte per MQTT inkl. Home Assistant Discovery veröffentlichen
- SHIP: Transport-/Session-Protokoll über TLS WebSocket.
- SPINE: Datenmodell/Anwendungsprotokoll, das in SHIP DATA Frames transportiert wird.
- SKI: Subject Key Identifier (Hex) aus dem X.509-Zertifikat – dient als stabile Client-Identität.
- EEBUS JSON / array-wrapped JSON: Manche Implementierungen kodieren JSON als Liste von Single-Key-Objekten.
This project contains a diagnostic Python script (connect_vr921.py) that talks to a Vaillant VR921/EEBUS gateway via SHIP (TLS WebSocket) and exchanges SPINE datagrams.
The script can:
- generate and reuse a client certificate (stable identity via SKI)
- announce itself and discover the VR921 via mDNS (
_ship._tcp.local.) - connect to wss://…/ship/ and run the SHIP handshake
- respond to gateway-initiated SPINE READ/CALL messages (required for interoperability)
- discover/subscribe to Measurement servers and read telemetry
- optionally publish telemetry via MQTT with Home Assistant Discovery
- SHIP: transport/session protocol over TLS WebSocket.
- SPINE: application data model/protocol transported inside SHIP DATA frames.
- SKI: X.509 Subject Key Identifier (hex) used as a stable client identity.
- EEBUS JSON / array-wrapped JSON: some stacks encode JSON as list-of-single-key objects.
- Python 3.10+ empfohlen
- Netzwerkzugriff auf den VR921 im selben Netzwerk (mDNS muss funktionieren)
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt- Wenn du MQTT nutzen willst:
paho-mqttist bereits in requirements.txt enthalten. - Broker-Zugangsdaten liegen in mqtt_secrets.py (standardmäßig in
.gitignore).
Du kannst das Skript dauerhaft auf einem Raspberry Pi (Raspberry Pi OS) oder einem Linux-Server im Netzwerk laufen lassen – typisch über systemd.
- Projekt z.B. nach
/opt/Vaillant-VR921kopieren und dort die venv anlegen:
sudo mkdir -p /opt/Vaillant-VR921
sudo chown -R $USER: /opt/Vaillant-VR921
cd /opt/Vaillant-VR921
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt- Optional: Umgebungsvariablen in eine Environment-Datei legen (empfohlen):
/etc/default/vaillant-vr921
# MQTT (optional)
HA_MQTT_HOST=
HA_MQTT_PORT=1883
HA_MQTT_USER=
HA_MQTT_PASSWORD=
# Logging (optional)
SHIP_JSONL=true
SHIP_DISCOVERY_LOG=false- systemd Service anlegen:
/etc/systemd/system/vaillant-vr921.service
[Unit]
Description=Vaillant VR921 SHIP/SPINE client
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=pi
WorkingDirectory=/opt/Vaillant-VR921
EnvironmentFile=-/etc/default/vaillant-vr921
ExecStart=/opt/Vaillant-VR921/.venv/bin/python /opt/Vaillant-VR921/connect_vr921.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target- Service aktivieren und starten:
sudo systemctl daemon-reload
sudo systemctl enable --now vaillant-vr921.service
sudo systemctl status vaillant-vr921.serviceLogs ansehen:
journalctl -u vaillant-vr921.service -f- Python 3.10+ recommended
- Network access to the VR921 on the same LAN (mDNS must work)
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt- If you want MQTT:
paho-mqttis included in requirements.txt. - Broker credentials are stored in mqtt_secrets.py (ignored by default via
.gitignore).
You can run the script continuously on a Raspberry Pi (Raspberry Pi OS) or a Linux server in your LAN – typically via systemd.
- Copy the project e.g. to
/opt/Vaillant-VR921and create a venv there:
sudo mkdir -p /opt/Vaillant-VR921
sudo chown -R $USER: /opt/Vaillant-VR921
cd /opt/Vaillant-VR921
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt- Optional: put environment variables into an env file (recommended):
/etc/default/vaillant-vr921
# MQTT (optional)
HA_MQTT_HOST=
HA_MQTT_PORT=1883
HA_MQTT_USER=
HA_MQTT_PASSWORD=
# Logging (optional)
SHIP_JSONL=true
SHIP_DISCOVERY_LOG=false- Create a systemd unit:
/etc/systemd/system/vaillant-vr921.service
[Unit]
Description=Vaillant VR921 SHIP/SPINE client
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=pi
WorkingDirectory=/opt/Vaillant-VR921
EnvironmentFile=-/etc/default/vaillant-vr921
ExecStart=/opt/Vaillant-VR921/.venv/bin/python /opt/Vaillant-VR921/connect_vr921.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target- Enable + start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now vaillant-vr921.service
sudo systemctl status vaillant-vr921.serviceView logs:
journalctl -u vaillant-vr921.service -fDu kannst MQTT auf zwei Arten konfigurieren:
- Datei mqtt_secrets.py ausfüllen
- oder Umgebungsvariablen setzen (überschreiben
mqtt_secrets.py)
Relevante Variablen:
HA_MQTT_HOST,HA_MQTT_PORT,HA_MQTT_USER,HA_MQTT_PASSWORDHA_MQTT_PREFIX(Defaulthomeassistant)HA_MQTT_STATE_PREFIX(Defaultship)SHIP_MQTT_DEBUG(True/False)SHIP_MQTT_RETAIN_STATE(True/False)
Zusätzlich:
HA_DEVICE_ID/HA_DEVICE_NAMEzur Identifikation in Home Assistant
SHIP_JSONL=truegibt Messwerte als JSONL (eine Zeile pro Update) aus.SHIP_DISCOVERY_LOG=truegibt zusätzliche Discovery-Infos aus.
You can configure MQTT in two ways:
- Fill in mqtt_secrets.py
- or set environment variables (they override
mqtt_secrets.py)
Relevant variables:
HA_MQTT_HOST,HA_MQTT_PORT,HA_MQTT_USER,HA_MQTT_PASSWORDHA_MQTT_PREFIX(defaulthomeassistant)HA_MQTT_STATE_PREFIX(defaultship)SHIP_MQTT_DEBUG(True/False)SHIP_MQTT_RETAIN_STATE(True/False)
Also:
HA_DEVICE_ID/HA_DEVICE_NAMEfor Home Assistant device naming
SHIP_JSONL=trueprints measurements as JSONL (one line per update).SHIP_DISCOVERY_LOG=trueprints extra discovery details.
-
MsgCounter- Zweck: Thread-/Async-sicherer Zähler für
msgCounterin SPINE Datagrammen. - Warum: SPINE erwartet pro Datagramm einen monoton steigenden Counter.
- Zweck: Thread-/Async-sicherer Zähler für
-
_env_str,_env_int,_env_bool- Zweck: Lesen von Umgebungsvariablen mit sinnvollen Defaults.
-
_slug- Zweck: Erzeugt sichere IDs für MQTT/HA (
object_id).
- Zweck: Erzeugt sichere IDs für MQTT/HA (
-
_unit_to_ha,_guess_ha_metadata,_friendly_sensor_name- Zweck: Best-Effort Mapping von SPINE scope/unit zu Home Assistant Sensor-Metadaten und Namen.
HAMqttPublisher- Zweck: Optionaler Publisher für MQTT + Home Assistant Discovery.
- Aktivierung: wenn
HA_MQTT_HOSTgesetzt ist (oder inmqtt_secrets.py). - Wichtige Methoden:
connect(): verbindet zum Broker und setzt LWT (availability).ensure_discovery(...): publiziert einmalig Discovery-Config für Sensoren.publish_state(...): publiziert Sensorwerte.close(): setzt offline und trennt.
-
json_into_eebus_json(...)- Zweck: Normales JSON → EEBUS „array-wrapped“ JSON.
- Hintergrund: Viele SHIP/SPINE Stacks erwarten diese Struktur.
-
json_text_into_eebus_json(...)- Zweck: Wie oben, aber von JSON-Text ausgehend (mit stabiler Feldreihenfolge).
-
json_from_eebus_json(...)- Zweck: EEBUS array-wrapped JSON → normales JSON (ship-go kompatible Ersetzung).
-
_first_cmd(...)- Zweck: Extrahiert das erste
cmdObjekt, egal obcmdals Dict, Liste oder verschachtelte Liste kommt.
- Zweck: Extrahiert das erste
-
_parse_spine_datagram(...)- Zweck: Aus einem SHIP DATA JSON
(header, first_cmd)extrahieren.
- Zweck: Aus einem SHIP DATA JSON
-
_make_spine_reply_addresses(...)- Zweck: Baut die korrekten Source/Destination Adressen für Reply/Result.
- Interop: Erzwingt nicht immer
device, weil manche Peers das als Fehler ansehen.
get_or_create_certificate()- Zweck: Erstellt/verwaltet
cert.pemundkey.pem. - Output: gibt die SKI (hex) zurück.
- Zweck: Erstellt/verwaltet
MDNSHandler- Zweck: Listener für
_ship._tcp.local.der den VR921 Kandidaten intarget_infospeichert.
- Zweck: Listener für
-
send_ship_json(...)- Zweck: SHIP CONTROL Frame (0x01) senden.
-
send_ship_data(...)- Zweck: SHIP DATA Frame (0x02) senden (ship-go kompatibles „payload placeholder“ Vorgehen).
-
send_access_methods(...)- Zweck: SHIP
accessMethodsmit lokaleridsenden.
- Zweck: SHIP
-
build_local_detailed_discovery(...)- Zweck: Minimale
NodeManagementDetailedDiscoveryDataAntwort.
- Zweck: Minimale
-
build_device_classification_manufacturer_data(...),build_device_classification_user_data(...)- Zweck: Minimale Antworten für DeviceClassification.
-
_spine_addr(...)- Zweck: Convenience Builder für SPINE Feature Address.
-
send_spine_read(...),send_spine_call(...)- Zweck: Baut und sendet SPINE Read/Call Datagramme (als SHIP DATA).
-
send_spine_result_ok(...)- Zweck: ACK über cmdClassifier=
result(errorNumber 0) mit msgCounterReference.
- Zweck: ACK über cmdClassifier=
-
handle_spine_read(...)- Zweck: Minimale Verarbeitung von
cmdClassifier=readund passende Replies.
- Zweck: Minimale Verarbeitung von
-
request_remote_detailed_discovery(...)- Zweck: Fordert vom VR921 die
nodeManagementDetailedDiscoveryDataan.
- Zweck: Fordert vom VR921 die
-
request_remote_node_management_use_case_data(...)- Zweck: Fordert
nodeManagementUseCaseDataan.
- Zweck: Fordert
-
_extract_entities(...),_extract_measurement_servers(...)- Zweck: Parse der Discovery, um Entities und Measurement Server zu finden.
-
subscribe_remote_measurement(...)- Zweck: Subscription via NodeManagementSubscriptionRequestCall.
-
request_remote_measurement_once(...)- Zweck: Einmaliges Lesen von
measurementDescriptionListDataundmeasurementListData.
- Zweck: Einmaliges Lesen von
-
parse_measurement_description(...),parse_measurement_list(...)- Zweck: Parsing der Reply/Notify Payloads zu strukturierten Updates.
-
perform_ship_handshake(...)- Zweck: Implementiert den SHIP Handshake als Zustandsmaschine:
- CMI Init
- HELLO (pending/ready)
- Protocol negotiation
- PIN (nur none)
- Access methods exchange
- Zweck: Implementiert den SHIP Handshake als Zustandsmaschine:
-
main()- Zweck: Orchestriert alles:
- Zertifikat/SKI
- mDNS announce + discovery
- Websocket connect + handshake
- Receive loop: SPINE ACKs, discovery, subscriptions, reads
- Optional MQTT Publish
- Zweck: Orchestriert alles:
-
MsgCounter- Purpose: async-safe counter for SPINE
msgCounter.
- Purpose: async-safe counter for SPINE
-
_env_str,_env_int,_env_bool- Purpose: read environment variables with safe defaults.
-
_slug- Purpose: build MQTT/HA-safe object ids.
-
_unit_to_ha,_guess_ha_metadata,_friendly_sensor_name- Purpose: best-effort mapping from SPINE scope/unit to HA metadata and names.
HAMqttPublisher- Purpose: optional MQTT publisher with Home Assistant Discovery.
- Enabled: if
HA_MQTT_HOSTis configured (or provided inmqtt_secrets.py). - Key methods:
connect(): connects and sets an LWT availability.ensure_discovery(...): publishes discovery config once per sensor.publish_state(...): publishes sensor values.close(): marks offline and disconnects.
-
json_into_eebus_json(...)- Purpose: convert normal JSON → EEBUS array-wrapped JSON.
-
json_text_into_eebus_json(...)- Purpose: same, starting from JSON text while preserving field order.
-
json_from_eebus_json(...)- Purpose: convert EEBUS array-wrapped JSON → normal JSON.
-
_first_cmd(...)- Purpose: extract the first
cmdobject regardless of nesting.
- Purpose: extract the first
-
_parse_spine_datagram(...)- Purpose: extract
(header, first_cmd)from a decoded SHIP DATA message.
- Purpose: extract
-
_make_spine_reply_addresses(...)- Purpose: compute correct source/destination for replies/results.
- Interop: does not always force-inject
device.
get_or_create_certificate()- Purpose: manage
cert.pem/key.pemand return the SKI hex.
- Purpose: manage
MDNSHandler- Purpose: listen for
_ship._tcp.local.and keep the VR921 candidate intarget_info.
- Purpose: listen for
-
send_ship_json(...)- Purpose: send SHIP CONTROL frames (0x01).
-
send_ship_data(...)- Purpose: send SHIP DATA frames (0x02) using a ship-go compatible placeholder approach.
-
send_access_methods(...)- Purpose: send SHIP
accessMethodswith local id.
- Purpose: send SHIP
-
build_local_detailed_discovery(...)- Purpose: minimal
NodeManagementDetailedDiscoveryDatareply.
- Purpose: minimal
-
build_device_classification_manufacturer_data(...),build_device_classification_user_data(...)- Purpose: minimal DeviceClassification replies.
-
_spine_addr(...)- Purpose: convenience SPINE address builder.
-
send_spine_read(...),send_spine_call(...)- Purpose: build + send SPINE read/call datagrams.
-
send_spine_result_ok(...)- Purpose: acknowledge a datagram via cmdClassifier=
result(errorNumber 0).
- Purpose: acknowledge a datagram via cmdClassifier=
-
handle_spine_read(...)- Purpose: minimal
cmdClassifier=readhandling and replies.
- Purpose: minimal
-
request_remote_detailed_discovery(...)- Purpose: request
nodeManagementDetailedDiscoveryDatafrom the VR921.
- Purpose: request
-
request_remote_node_management_use_case_data(...)- Purpose: request
nodeManagementUseCaseData.
- Purpose: request
-
_extract_entities(...),_extract_measurement_servers(...)- Purpose: parse discovery to list entities and measurement servers.
-
subscribe_remote_measurement(...)- Purpose: subscription via NodeManagementSubscriptionRequestCall.
-
request_remote_measurement_once(...)- Purpose: read
measurementDescriptionListDataandmeasurementListDataonce.
- Purpose: read
-
parse_measurement_description(...),parse_measurement_list(...)- Purpose: parse reply/notify payloads into structured updates.
-
perform_ship_handshake(...)- Purpose: implements SHIP handshake state machine:
- CMI init
- HELLO (pending/ready)
- protocol negotiation
- PIN (only none)
- access methods exchange
- Purpose: implements SHIP handshake state machine:
-
main()- Purpose: orchestrates everything end-to-end.
python3 connect_vr921.pyWährend HELLO phase=pending musst du in der myVAILLANT App den Zugriff/Trust bestätigen.
python3 connect_vr921.pyWhile HELLO phase=pending, confirm Trust/Pairing in the myVAILLANT app.
graph TD
%% Tier 1: Device
Device[<b>Tier 1: Device</b><br/>Vaillant VR921 Gateway<br/>ID: 212232...6209]
%% Tier 2: Entities
subgraph Entities [<b>Tier 2: Entities</b>]
E0[entity=0<br/>Device Information]
E3[entity=3<br/>HeatPump Appliance]
E31[entity=3,1<br/>Compressor]
E4[entity=4<br/>DHW Circuit<br/>Warmwasser]
E511[entity=5,1,1<br/>HVAC Room<br/>Heizkreis]
E6[entity=6<br/>Temp Sensor<br/>Außenfühler]
end
%% Tier 3: Features
subgraph Features [<b>Tier 3: Features</b>]
F11_C[feature=11<br/>Measurement<br/>Power/Energy]
F19[feature=19<br/>SmartEnergy<br/>PV-Optimization]
F11_W[feature=11<br/>Measurement<br/>Ist-Temp]
F18_W[feature=18<br/>Setpoint<br/>Soll-Temp]
F11_R[feature=11<br/>Measurement<br/>Zimmer-Temp]
F18_R[feature=18<br/>Setpoint<br/>Soll-Temp]
F11_A[feature=11<br/>Measurement<br/>Außen-Temp]
end
%% Verbindungen
Device --> E0
Device --> E3
E3 --> E31
Device --> E4
Device --> E511
Device --> E6
E31 --> F11_C
E31 --> F19
E4 --> F11_W
E4 --> F18_W
E511 --> F11_R
E511 --> F18_R
E6 --> F11_A
%% Styling
style Device fill:#f9f,stroke:#333,stroke-width:2px
style Entities fill:#fff,stroke:#333,stroke-dasharray: 5 5
style Features fill:#dfd,stroke:#333,stroke-width:1px