← alle beiträge

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.

  1. Modbus-CLI-Tools

    sudo apt-get install libmodbus-dev
    # Oder modbus-utils für einfache CLI-Tools
    pip install pymodbus[cli]
    
  2. Python und PyModbus (fürs Skripten)

    pip install pymodbus
    
  3. Netzwerk-Scanner (für Modbus über TCP)

    sudo apt-get install nmap
    
  4. Serielle Tools (für RTU-Modbus)

    sudo apt-get install minicom screen
    
  5. Optional: Wireshark (zur Protokollanalyse)

    sudo apt-get install wireshark
    

Die Grundlagen

Modbus-Datenmodell

Modbus kennt vier Registersorten:

TypZugriffGrößeZweck
Coils (0x)Lesen/Schreiben1 BitDigitale Ausgänge (Relais, Schalter)
Discrete Inputs (1x)Nur lesen1 BitDigitale Eingänge (Sensoren, Schalter)
Input Registers (3x)Nur lesen16 BitAnaloge Eingänge (Messwerte)
Holding Registers (4x)Lesen/Schreiben16 BitKonfiguration, Sollwerte

Funktionscodes

Mit Funktionscodes sagst du, was passieren soll:

  • 0x01 - Coils lesen
  • 0x02 - Discrete Inputs lesen
  • 0x03 - Holding Registers lesen
  • 0x04 - Input Registers lesen
  • 0x05 - Einzelnes Coil schreiben
  • 0x06 - 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.