Zum Inhalt

Workshop: Industrielles RS-485 Modbus Energie-Gateway

In Industrieanlagen, Rechenzentren und Photovoltaik-Parks ist eine kontinuierliche Erfassung elektrischer Leistungswerte unerlässlich für Lastmanagement, Kostenstellenabrechnung und Anomalie-Erkennung. Einfache Smart-Plugs aus dem Heimbereich scheiden in Schaltschränken aus: Sie beherrschen keine 3-phasigen 400-V-Netze und besitzen keine galvanische Trennung.

In diesem praxisorientierten Workshop verwandeln wir einen Raspberry Pi (Raspberry Pi OS 64-Bit Bookworm) in ein industrielles IoT-Energie-Gateway. Wir binden einen geeichten Hutschienen-Stromzähler (Eastron SDM630 / SDM120) über einen differentiellen RS-485-Bus mit Modbus RTU an, protokollieren elektrische Kennzahlen hochfrequent in SQLite (WAL-Modus) und publizieren strukturierte Messwerte per MQTT für Dashboards in Grafana.


1. Materialliste & Elektrische Sicherheit

%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
    subgraph Cabinet["Hauptverteilung (400V / 230V AC)"]
        direction TB
        GRID["3-Phasen-Netz<br/>(L1, L2, L3, N)"]
        METER["Hutschienen-Zaehler<br/>(Eastron SDM630)"]
        LOAD["Elektrische Last"]
        GRID --> METER
        LOAD --- METER
    end

    subgraph Bus["Industrieller Bus (RS-485)"]
        direction TB
        TRANS["SP3485 3,3V Transceiver"]
        TERM["120 Ω Bus-Abschluss"]
        TRANS --- TERM
    end

    subgraph Gateway["IoT-Gateway (Raspberry Pi)"]
        direction TB
        RPI["Raspberry Pi Host<br/>(Debian Bookworm)"]
        DB["SQLite WAL-Datenbank<br/>(/data/energie.db)"]
        MQTT["MQTT-Broker / Grafana"]
        RPI --> DB
        RPI --> MQTT
    end

    Cabinet -->|Adernpaar A/B| Bus
    Bus -->|UART GPIO 14/15| Gateway

Benötigte Hardware-Komponenten

  1. Host-Computer: Raspberry Pi 4 (ab 2 GB) oder Raspberry Pi 5.
  2. Hutschienen-Stromzähler: Eastron SDM630 Modbus-V2 (3-phasig Drehstrom) oder Eastron SDM120 Modbus (1-phasig 230 V).
  3. Differentieller Transceiver: SP3485 (nativer 3,3-V-Transceiver) oder ein galvanisch getrenntes RS-485 HAT (z. B. Waveshare 2-CH RS485 HAT).
  4. Verkabelung: 24 AWG geschirmtes verdrilltes Adernpaar (Belden 9841 oder Cat6-Adernpärchen) sowie zwei 120-Ω-Abschlusswiderstände (0,25 W Metallschicht).
  5. Netzteil: 5 V / 3 A USB-C-Netzteil oder 27 W Raspberry Pi Netzteil.

Lebensgefahr durch Netzspannung (400 V / 230 V AC)

Netzspannungen sind lebensgefährlich! Die Installation des Hutschienen-Messgeräts in der Unterverteilung darf ausnahmslos durch eine zertifizierte Elektrofachkraft bei freigeschaltetem Hauptschalter erfolgen. Die RS-485-Datenklemmen (A, B, G) arbeiten mit Schutzkleinspannung (< 5 V) und sind galvanisch vom Stromnetz getrennt.


2. Hardware-Verdrahtung & Transceiver-Anschluss

Den SP3485-Transceiver mit der 40-Pin-Leiste des Raspberry Pi verbinden:

SP3485-Pin Raspberry Pi Pin Funktion
VCC Pin 1 (3,3 V) Logik-Spannungsversorgung
GND Pin 6 (GND) Masse
TXD (DI) Pin 8 (BCM GPIO 14 / TXD) Serielle Sendedaten zum Transceiver
RXD (RO) Pin 10 (BCM GPIO 15 / RXD) Serielle Empfangsdaten vom Transceiver
DE & RE Pin 12 (BCM GPIO 18) Richtungsumschaltung (HIGH=Senden, LOW=Empfangen)
Klemme A Stromzähler Klemme A (+) Differenzielle Datenleitung +
Klemme B Stromzähler Klemme B (-) Differenzielle Datenleitung -
Schirm / G Schutzerde (nur einseitig auflegen) Kabelschirm gegen Störungen

Bus-Terminierung

Schalte jeweils einen 120-Ω-Widerstand parallel zwischen Klemme A und B direkt am Stromzähler sowie am Transceiver des Raspberry Pi.


3. Host-Konfiguration: Kernel-UART & Pakete

Schritt 1: PL011-Hardware-UART freischalten

Bluetooth deaktivieren und den vollwertigen PL011-UART auf die Pins 14/15 legen:

In /boot/firmware/config.txt ergänzen:

enable_uart=1
dtoverlay=disable-bt

In /boot/firmware/cmdline.txt den Parameter console=serial0,115200 entfernen.

System neu starten:

sudo reboot

Schritt 2: Python-Bibliotheken installieren

Erforderliche Bibliotheken für Modbus, SQLite und MQTT installieren:

sudo apt update
sudo apt install -y python3-serial python3-pip sqlite3 mosquitto-clients
pip3 install --break-system-packages minimalmodbus paho-mqtt

4. Modbus-RTU Registerbelegung (Eastron SDM-Serie)

Die Eastron SDM-Zähler kommunizieren standardmäßig mit: Baudrate 9600, 8 Datenbits, keine Parität, 1 Stoppbit (8N1) und Slave-ID 1.

Messwerte werden als 32-Bit IEEE 754 Gleitkommazahlen über zwei aufeinanderfolgende 16-Bit-Input-Register übertragen (Funktionscode 0x04):

Messgröße Register-Adresse (Hex) Register-Anzahl Datentyp Einheit
Phase 1 Spannung 0x0000 2 32-Bit Float Volt (V)
Phase 2 Spannung 0x0002 2 32-Bit Float Volt (V)
Phase 3 Spannung 0x0004 2 32-Bit Float Volt (V)
Phase 1 Stromstärke 0x0006 2 32-Bit Float Ampere (A)
Gesamte Wirkleistung 0x0034 2 32-Bit Float Watt (W)
Netzfrequenz 0x0046 2 32-Bit Float Hertz (Hz)
Gesamtenergie (Import) 0x0048 2 32-Bit Float Kilowattstunden (kWh)

5. Gateway-Implementierung: Modbus zu SQLite & MQTT

Skript /usr/local/bin/energy_telemetry_gateway.py anlegen:

#!/usr/bin/env python3
"""
Industrielles Modbus RTU Energie-Gateway
Liest Messwerte vom Eastron SDM630 ueber RS-485 ein,
speichert sie in SQLite (WAL-Modus) und publiziert JSON per MQTT.
"""

import time
import json
import sqlite3
import logging
import minimalmodbus
import serial
import paho.mqtt.client as mqtt

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")

SERIAL_PORT = "/dev/serial0"
SLAVE_ADDRESS = 1
BAUD_RATE = 9600
DB_FILE = "/data/energie_telemetrie.db"
MQTT_BROKER = "192.168.10.10"
MQTT_PORT = 1883
MQTT_TOPIC = "industrie/strom/verteiler_01"

class EnergyGateway:
    def __init__(self):
        # Modbus-Schnittstelle konfigurieren
        self.meter = minimalmodbus.Instrument(SERIAL_PORT, SLAVE_ADDRESS)
        self.meter.serial.baudrate = BAUD_RATE
        self.meter.serial.bytesize = 8
        self.meter.serial.parity = serial.PARITY_NONE
        self.meter.serial.stopbits = 1
        self.meter.serial.timeout = 0.5
        self.meter.mode = minimalmodbus.MODE_RTU
        self.meter.clear_buffers_before_each_transaction = True

        # SQLite-Datenbank initialisieren
        self._init_database()

        # MQTT-Client vorbereiten
        self.mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, "rpi-energie-gateway")
        try:
            self.mqtt_client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60)
            self.mqtt_client.loop_start()
            logging.info(f"Verbunden mit MQTT-Broker unter {MQTT_BROKER}:{MQTT_PORT}")
        except Exception as err:
            logging.warning(f"MQTT-Verbindung nicht moeglich ({err}). Laeuft im lokalen Modus.")

    def _init_database(self) -> None:
        conn = sqlite3.connect(DB_FILE)
        cursor = conn.cursor()
        cursor.execute("PRAGMA journal_mode=WAL;")
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS messungen (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
                spannung_l1 REAL,
                spannung_l2 REAL,
                spannung_l3 REAL,
                strom_l1 REAL,
                wirkleistung_w REAL,
                frequenz_hz REAL,
                arbeit_kwh REAL
            );
        """)
        conn.commit()
        conn.close()
        logging.info(f"SQLite-Datenbank unter {DB_FILE} (WAL-Modus) initialisiert.")

    def read_metrics(self) -> dict:
        """Liest 32-Bit Gleitkomma-Register per Funktionscode 0x04."""
        v_l1 = self.meter.read_float(0x0000, functioncode=4, number_of_registers=2)
        v_l2 = self.meter.read_float(0x0002, functioncode=4, number_of_registers=2)
        v_l3 = self.meter.read_float(0x0004, functioncode=4, number_of_registers=2)
        i_l1 = self.meter.read_float(0x0006, functioncode=4, number_of_registers=2)
        power = self.meter.read_float(0x0034, functioncode=4, number_of_registers=2)
        freq = self.meter.read_float(0x0046, functioncode=4, number_of_registers=2)
        kwh = self.meter.read_float(0x0048, functioncode=4, number_of_registers=2)

        return {
            "timestamp": int(time.time()),
            "spannung_l1": round(v_l1, 1),
            "spannung_l2": round(v_l2, 1),
            "spannung_l3": round(v_l3, 1),
            "strom_l1": round(i_l1, 2),
            "wirkleistung_w": round(power, 1),
            "frequenz_hz": round(freq, 2),
            "arbeit_kwh": round(kwh, 2)
        }

    def persist_and_publish(self, data: dict) -> None:
        # In SQLite sichern
        conn = sqlite3.connect(DB_FILE)
        cursor = conn.cursor()
        cursor.execute("""
            INSERT INTO messungen (spannung_l1, spannung_l2, spannung_l3, strom_l1, wirkleistung_w, frequenz_hz, arbeit_kwh)
            VALUES (?, ?, ?, ?, ?, ?, ?);
        """, (data["spannung_l1"], data["spannung_l2"], data["spannung_l3"], data["strom_l1"], data["wirkleistung_w"], data["frequenz_hz"], data["arbeit_kwh"]))
        conn.commit()
        conn.close()

        # Per MQTT senden
        payload = json.dumps(data)
        self.mqtt_client.publish(MQTT_TOPIC, payload, qos=1)

    def run(self) -> None:
        logging.info("Starte Messwerterfassung (Zyklus: 1,0s)...")
        while True:
            try:
                metrics = self.read_metrics()
                self.persist_and_publish(metrics)
                logging.info(f"Netz: {metrics['spannung_l1']}V | Last: {metrics['wirkleistung_w']}W | Frequenz: {metrics['frequenz_hz']}Hz | Zaehlerstand: {metrics['arbeit_kwh']} kWh")
            except (minimalmodbus.NoResponseError, minimalmodbus.InvalidResponseError) as err:
                logging.warning(f"Modbus-Uebertragungsfehler: {err}")
            except Exception as err:
                logging.error(f"Unerwarteter Fehler: {err}")

            time.sleep(1.0)

if __name__ == "__main__":
    gateway = EnergyGateway()
    gateway.run()

6. Systemd-Dienst für 24/7-Dauerbetrieb

Dienstdatei /etc/systemd/system/energy-gateway.service erstellen:

[Unit]
Description=Industrielles RS-485 Modbus Energie-Gateway
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=root
ExecStart=/usr/bin/python3 /usr/local/bin/energy_telemetry_gateway.py
Restart=always
RestartSec=5s
KillMode=process

# System-Haertung
ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

Dienst aktivieren und starten:

sudo systemctl daemon-reload
sudo systemctl enable --now energy-gateway.service

Logs im Live-Stream verfolgen:

sudo journalctl -u energy-gateway.service -f

7. Fehlerbehebung in der Praxis

Fehlermeldung Häufige Ursache Behebung
NoResponseError Vertauschte A/B-Leitungen Leitungen an Klemme A und B am Transceiver tauschen.
InvalidResponseError Fehlende Abschlusswiderstände 120-Ω-Widerstände an beiden Leitungsenden prüfen.
Permission Denied /dev/serial0 Benutzer nicht in dialout-Gruppe sudo usermod -aG dialout $USER ausführen.
Negative Leistungswerte Stromwandler (CT) falsch herum Pfeil auf dem Messwandler muss in Richtung Verbraucher zeigen.

Nächste Schritte

Mit dem industriellen Feldbus-Gateway im Betrieb kannst du deine Edge-Umgebung vervollständigen: