Mit Modbus spielen
/ Nov. 2025, modbus, hardware, python
Als mich ein Freund bat, ihm beim Auslesen seines Wechselrichters zu helfen, dachte ich: kein Ding, ’ne Stunde, höchstens. Tja. Der deutsche Hersteller hatte sein System verrammelt wie einen Tresor: über die eigene Software rückte es so gut wie nichts raus. Nach einigem Graben fand ich heraus: Modbus ist da, aber komplett undokumentiert. Was folgte, war ein Trip durch industrielle Kommunikationsprotokolle, Reverse Engineering und die hohe Kunst, störrischer Hardware das Sprechen beizubringen.
Was ist Modbus?
Modbus ist ein industrielles Kommunikationsprotokoll von 1979, längst De-facto-Standard zum Vernetzen von Industriegeräten. Simpel, robust, überall unterstützt. Vermutlich läuft es genau deshalb bis heute, selbst in modernen Wechselrichtern.
Das Prinzip ist Master-Slave, also:
- Master (dein Rechner) fragt an
- Slave (der Wechselrichter) antwortet
- Geredet wird seriell (RTU) oder über TCP/IP
- Daten liegen in Registern, jedes mit seiner Bedeutung
Voraussetzungen
Bevor es losgeht, brauchst du Werkzeug. Ich bleibe bei Linux/Unix. Für Windows gibt es Entsprechungen, versprochen.
-
Modbus-CLI-Tools
sudo apt-get install libmodbus-dev # Oder modbus-utils für einfache CLI-Tools pip install pymodbus[cli] -
Python und PyModbus (fürs Skripten)
pip install pymodbus -
Netzwerk-Scanner (für Modbus über TCP)
sudo apt-get install nmap -
Serielle Tools (für RTU-Modbus)
sudo apt-get install minicom screen -
Optional: Wireshark (zur Protokollanalyse)
sudo apt-get install wireshark
Die Grundlagen
Modbus-Datenmodell
Modbus kennt vier Registersorten:
| Typ | Zugriff | Größe | Zweck |
|---|---|---|---|
| Coils (0x) | Lesen/Schreiben | 1 Bit | Digitale Ausgänge (Relais, Schalter) |
| Discrete Inputs (1x) | Nur lesen | 1 Bit | Digitale Eingänge (Sensoren, Schalter) |
| Input Registers (3x) | Nur lesen | 16 Bit | Analoge Eingänge (Messwerte) |
| Holding Registers (4x) | Lesen/Schreiben | 16 Bit | Konfiguration, Sollwerte |
Funktionscodes
Mit Funktionscodes sagst du, was passieren soll:
0x01- Coils lesen0x02- Discrete Inputs lesen0x03- Holding Registers lesen0x04- Input Registers lesen0x05- Einzelnes Coil schreiben0x06- Einzelnes Register schreiben
Register-Adressierung
Hier wird’s fies. Modbus hat mehrere Adresskonventionen, und sie beißen sich:
- Protokolladressen: 0-basiert (0, 1, 2, …)
- Datenmodell-Adressen: 1-basiert (1, 2, 3, …)
- Registerreferenzen: Typ-Präfix plus Adresse (30001, 40001, …)
Die meisten Tools reden Protokolladressen (0-basiert). Dokus dagegen gern Datenmodell. Viel Spaß beim Umrechnen.
Die Spurensuche
Schritt 1: Wer ist im Netz?
Zuerst musste der Wechselrichter im Netz gefunden werden. Moderne Geräte sprechen Modbus-TCP auf Port 502. Meistens jedenfalls.
# Geräte mit offenem Modbus-Port suchen
nmap -p 502 192.168.1.0/24
# Gezielt eine IP prüfen
nmap -p 502 192.168.1.100
Schritt 2: Erster Verbindungstest
IP gefunden? Dann erst mal vorsichtig anklopfen:
# Verbindung per Modbus-CLI testen
modbus read 192.168.1.100 502 1 0 10
# Oder schnell in Python
python3 -c "
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('192.168.1.100')
result = client.read_holding_registers(0, 10, slave=1)
print(result.registers if not result.isError() else 'Error')
client.close()
"
Schritt 3: Register entdecken
Das hier war die eigentliche Detektivarbeit. Ohne Doku heißt es: Bereiche auslesen und mit dem Display des Wechselrichters abgleichen, bis die Werte einen Sinn ergeben.
# Registerbereiche lesen, um Daten zu finden
for i in {0..100..10}; do
echo "Registers $i-$((i+9)):"
modbus read 192.168.1.100 502 1 $i 10
sleep 1
done
Schritt 4: Daten deuten
Rohwerte allein sagen wenig. Wechselrichter speichern typischerweise:
- Leistungswerte: gern in Watt oder Kilowatt
- Spannung/Strom: meist mit Skalierungsfaktor
- Energiezähler: aufsummierte Werte
- Statuscodes: Bitflags oder Aufzählungen
Ich hab mir eine Mapping-Tabelle gebaut, indem ich aufs Display gestarrt und Registerwerte danebengelegt habe. Klingt stumpf? Ist es auch. Funktioniert aber.
# Beispiel-Mapping, durch Ausprobieren gefunden
REGISTER_MAP = {
40001: {"name": "AC_Power", "unit": "W", "scale": 1},
40002: {"name": "AC_Voltage_L1", "unit": "V", "scale": 0.1},
40003: {"name": "AC_Current_L1", "unit": "A", "scale": 0.01},
40010: {"name": "DC_Voltage", "unit": "V", "scale": 0.1},
40011: {"name": "DC_Current", "unit": "A", "scale": 0.01},
40020: {"name": "Total_Energy", "unit": "kWh", "scale": 0.1},
}
Die Python-Lösung
Mit kartierten Registern kam ein kleines Skript zum Auslesen und Deuten der Daten:
#!/usr/bin/env python3
from pymodbus.client import ModbusTcpClient
import time
import json
from datetime import datetime
class InverterReader:
def __init__(self, host, port=502, slave_id=1):
self.client = ModbusTcpClient(host, port=port)
self.slave_id = slave_id
# Register mapping discovered through investigation
self.registers = {
0: {"name": "AC_Power", "unit": "W", "scale": 1},
1: {"name": "AC_Voltage_L1", "unit": "V", "scale": 0.1},
2: {"name": "AC_Current_L1", "unit": "A", "scale": 0.01},
9: {"name": "DC_Voltage", "unit": "V", "scale": 0.1},
10: {"name": "DC_Current", "unit": "A", "scale": 0.01},
19: {"name": "Total_Energy", "unit": "kWh", "scale": 0.1},
30: {"name": "Inverter_Temperature", "unit": "°C", "scale": 0.1},
}
def connect(self):
"""Establish connection to the inverter"""
return self.client.connect()
def read_register(self, address):
"""Read a single holding register"""
try:
result = self.client.read_holding_registers(address, 1, slave=self.slave_id)
if result.isError():
return None
return result.registers[0]
except Exception as e:
print(f"Error reading register {address}: {e}")
return None
def read_all_data(self):
"""Read all mapped registers and return formatted data"""
data = {
"timestamp": datetime.now().isoformat(),
"readings": {}
}
for address, config in self.registers.items():
raw_value = self.read_register(address)
if raw_value is not None:
# Apply scaling factor
scaled_value = raw_value * config["scale"]
data["readings"][config["name"]] = {
"value": scaled_value,
"unit": config["unit"],
"raw": raw_value
}
return data
def monitor(self, interval=30):
"""Continuously monitor and log data"""
print("Starting inverter monitoring...")
while True:
try:
data = self.read_all_data()
# Print current readings
print(f"\n--- {data['timestamp']} ---")
for name, reading in data["readings"].items():
print(f"{name}: {reading['value']} {reading['unit']}")
# Optionally save to file
with open("inverter_data.json", "a") as f:
f.write(json.dumps(data) + "\n")
time.sleep(interval)
except KeyboardInterrupt:
print("\nMonitoring stopped by user")
break
except Exception as e:
print(f"Error during monitoring: {e}")
time.sleep(5) # Wait before retrying
def close(self):
"""Close the connection"""
self.client.close()
# Usage example
if __name__ == "__main__":
inverter = InverterReader("192.168.1.100")
if inverter.connect():
print("Connected to inverter successfully!")
# Read current data
current_data = inverter.read_all_data()
print(json.dumps(current_data, indent=2))
# Start monitoring (uncomment to run)
# inverter.monitor(interval=60)
else:
print("Failed to connect to inverter")
inverter.close()
Gelernt
1. Register finden ist Detektivarbeit
Ohne Doku brauchst du Geduld und System. Bewährt hat sich bei mir:
- Registerwerte mit dem Display abgleichen
- Unter verschiedenen Bedingungen testen (Tag/Nacht, Lastwechsel)
- Nach Mustern in aufeinanderfolgenden Registern suchen
- Schauen, welche Konventionen andere Hersteller so pflegen
2. Skalierung zählt
Rohwerte lügen fast immer. 2350 im Register heißt gern 235.0V oder 23.50A. Der Faktor ist meist ’ne Zehnerpotenz (0.1, 0.01, …). Wer das vergisst, misst Mist.
3. Fehlerbehandlung ist Pflicht
Industrienetze sind laut. Also bitte:
- Verbindungs-Retries einbauen
- Timeouts für Lesezugriffe
- Daten plausibilisieren (Sanity-Checks)
- Würdevoll degradieren, wenn einzelne Register streiken
4. Alles dokumentieren
Schreib auf, was du findest. Wirklich. Eine Registertabelle mit:
- Adressen
- Datentypen und Skalierung
- Gültigen Bereichen
- Update-Häufigkeiten
- Eigenarten und Sonderverhalten
Dein zukünftiges Ich bedankt sich. Meins tut’s jedenfalls.
Über die Grundlagen hinaus
Läuft die Kommunikation erst mal, lässt sich darauf aufbauen:
- Datenbank-Logging: Historie in InfluxDB oder SQLite kippen
- Web-Dashboard: Monitoring-Oberfläche in Echtzeit
- Alarme: Bescheid sagen, wenn Werte aus dem Ruder laufen
- Integration: An die Hausautomatisierung andocken
- Analyse: Reports über Ertrag und Effizienz generieren
Der Trick ist, wie eigentlich immer, klein anfangen und dann Schicht für Schicht drauflegen.
Fazit
Undokumentierte Modbus-Geräte haben mich gelehrt: Hartnäckigkeit zahlt sich aus. Was als Gefallen für einen Freund anfing, wurde ein Tiefenstieg in Industrieprotokolle und Reverse Engineering. CLI-Tools zum Aufspüren, Python zum Automatisieren. Die Kombi hat den störrischen deutschen Wechselrichter am Ende doch gezähmt.
Das Schöne an Modbus ist seine Einfachheit. Wer die Grundlagen draufhat, redet mit tausenden Industriegeräten. Die Hürde ist selten das Protokoll selbst, sondern herauszufinden, was ein Gerät über seine Register überhaupt preisgibt.