Solarstrom im Blick: Aktuelle Daten eines Kostal-Wechselrichters mit Modbus auslesen

28. September 2026

Soll ich jetzt die Waschmaschine starten oder lieber noch warten? Ein kleines Python-Programm beantwortet diese Frage in einem Augenblick. Es zeigt in einem übersichtlichen Fenster, wie voll die Batterie ist, wie viel Solarstrom gerade erzeugt wird, was das Haus verbraucht und ob Strom ins Netz fließt oder aus dem Netz kommt. Es liest die Daten direkt aus dem Kostal-Plenticore-Wechselrichter, ist schlank genug für den Dauerbetrieb und lässt sich mit wenigen Zeilen Python erweitern, zum Beispiel um einen Heizlüfter einzuschalten, wenn Strom im Überfluss vorhanden ist.

Die notwendigen Pythonskripte in einer Zip-Datei: Kostal Plenticore Modbus

Was das Programm macht: Das Programm öffnet ein kleines Fenster mit hellgelbem Hintergrund und aktualisiert es alle zwei Sekunden. Oben stehen Batterietemperatur, Ladezustand in Prozent, die gespeicherte Energie in Kilowattstunden und die Leistung der Batterie. Diese Zahl wird rot, wenn die Batterie gerade Strom abgibt, sodass man sofort sieht, ob das Haus vom Speicher lebt. Darunter folgen die Leistung der beiden Solarstränge, die gesamte Solarleistung, der gesamte Hausverbrauch und der Netzbezug beziehungsweise die Einspeisung. Ein hellblauer Block zeigt für die drei Phasen L1, L2 und L3 die Netzspannung, den Strom und die Leistung sowie die Netzfrequenz auf drei Nachkommastellen. Wer nachts einen Blick auf die drei Leistungen wirft, sieht sofort, ob irgendwo noch ein Licht oder ein Gerät läuft. In der Statuszeile ganz unten steht, ob die Verbindung zum Wechselrichter funktioniert, und bei Problemen die Fehlermeldung in Rot. Die nutzbare Batteriekapazität steht fest im Programm (in meinem Fall 12,8 Kilowattstunden mit fünf Prozent Reserve). Wer einen anderen Speicher hat, ändert diese Zahl in der Zeile, die die gespeicherte Energie berechnet.

Kostal Plenticore Plus 10 Kw
Plenticore Wechselrichter von Kostal

So liest das Programm die Daten aus: Die Kommunikation mit dem Wechselrichter steckt in einer eigenen Datei, kostal_modbusquery.py. Sie stammt von Kilian Knoll (Copyright 2018, GPL) und basiert auf der Schnittstellenbeschreibung, die Kostal für seine Wechselrichter veröffentlicht hat. Mit dem Python-Modul pymodbus baut sie eine TCP-Verbindung zum Wechselrichter auf, standardmäßig über Port 1502 und die Geräteadresse 71, und liest Register für Register nur lesend aus. Jedes Register ist in einer Liste mit Adresse, Bezeichnung, Datentyp und Wert abgelegt. Mein Hauptprogramm macht daraus ein Wörterbuch, in dem man jeden Wert über seinen Namen abrufen kann, etwa Power DC1 oder Act. state of charge. Daraus berechnet es die Summen und aktualisiert die Anzeige. Eine Erfahrung aus meinem früheren Artikel über die telefonische Ansage der Solardaten mit Asterisk gilt auch hier: Der Wert Inverter Generation Power (actual) war bei mir unsinnig hoch. Deshalb berechne ich Netzbezug und Einspeisung aus der Summe der Wirkleistungen der drei Phasen des Leistungsmessers am Hausanschluss. Positive Werte bedeuten Bezug, negative Einspeisung.

Für welche Wechselrichter das Programm geeignet ist: Ich habe das Programm mit einem Kostal Plenticore Plus 10 kW mit angeschlossener Batterie entwickelt und getestet. Die Registerbelegung stammt aus der Kostal-Dokumentation für die Plenticore-Plus-Reihe. Bei verwandten Geräten von Kostal, die dieselbe Modbus-Beschreibung verwenden, dürfte es ebenfalls funktionieren, aber das habe ich nicht selbst geprüft. Wechselrichter anderer Hersteller haben eine andere Registerbelegung, für sie ist das Programm nicht geeignet. Wer keinen Batteriespeicher hat, kann die Batteriezeilen ignorieren oder aus dem Programm streichen.

Modbuskostalplenticore1
Weboberfläche eines Plenticore Wechselrichters von Kostal. So kommt man auf die Einstellungen für den Modbus.
Modbusplenticore2
Modbus aktivieren.

Modbus im Wechselrichter einschalten: Zuerst braucht der Wechselrichter eine feste IP-Adresse im Heimnetz. Das stellt man im Webportal des Wechselrichters unter Einstellungen, Netzwerk ein, alternativ reserviert man die Adresse im Router. In meinem Beispiel ist es die fiktive Adresse 192.168.1.222. Danach schaltet man unter Einstellungen, Modbus / Sunspec (TCP) die Option Modbus aktivieren ein und speichert. Die übrigen Grundeinstellungen habe ich nicht verändert. Wichtig ist, dass der dort eingetragene Port mit dem Port in der Datei kostal_modbusquery.py übereinstimmt (Voreinstellung 1502) und dass die Geräteadresse zum Programm passt. Im Programm ist 71 eingetragen, was der Standardeinstellung entspricht.

Bildschirmfoto zu 2026 09 28 18 55
Das keine Pythonprogramm läuft unter Linux und Windows (siehe Text).

Was in die kostal_modbusquery.py eingetragen wird: Am Anfang der Klasse stehen zwei Zeilen, in denen man die Adresse des Wechselrichters und den Port einträgt, also inverter_ip und inverter_port. Läuft das Programm im selben Heimnetz wie der Wechselrichter, und das ist der Normalfall, trägt man dort die lokale IP-Adresse ein, im Beispiel 192.168.1.222. Das ist die einfachste und sicherste Lösung. Ein DynDNS-Name oder eine öffentliche IP-Adresse kommt nur infrage, wenn das Programm außerhalb des Heimnetzes läuft und der Router den Port 1502 an den Wechselrichter weiterleitet. Davon rate ich ab. Modbus kennt weder Passwort noch Verschlüsselung, ein offen ins Internet gestellter Port ist eine Einladung für jeden, der ihn findet. Wer von unterwegs zugreifen will, sollte einen VPN-Tunnel wie WireGuard einrichten und dann ebenfalls die lokale Adresse verwenden. Nebenbei: Ein DynDNS-Name funktioniert von innen oft gar nicht, weil viele Router Anfragen an die eigene öffentliche Adresse nicht zurück ins Heimnetz umleiten. Im Router die Portweiterleitung für den Port 1502 nicht vergessen.

Der Quellcode: Die kostal_modbusquery.py muss im selben Verzeichnis stehen.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk
import kostal_modbusquery  # Import der separaten Kostal-Datei

HELLGELB = "#FFFFCC"
HELLBLAU = "#ADD8E6"


class KostalGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Kostal Solaranlage Monitor")
        self.root.geometry("480x620")  # Fenstergröße angepasst für den Phasenblock
        self.root.configure(bg=HELLGELB)

        # Kostal Query Objekt initialisieren
        self.kostal = kostal_modbusquery.kostal_modbusquery()

        # GUI Elemente erstellen
        self.create_widgets()

        # Erstes Update starten
        self.update_data()

    def create_widgets(self):
        # Stile konfigurieren
        style = ttk.Style()
        style.configure("My.TFrame", background=HELLGELB)
        style.configure("My.TLabel", font=("Helvetica", 12), background=HELLGELB)
        style.configure("Blau.TFrame", background=HELLBLAU)
        style.configure("Blau.TLabel", font=("Helvetica", 12), background=HELLBLAU)
        style.configure("BlauFett.TLabel", font=("Helvetica", 12, "bold"), background=HELLBLAU)

        # Rahmen für die Anzeige
        main_frame = ttk.Frame(self.root, padding="15", style="My.TFrame")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        # Labels und Werte
        self.labels = {
            "Battery temperature": ttk.Label(main_frame, text="Batterietemperatur:", style="My.TLabel"),
            "Act. state of charge": ttk.Label(main_frame, text="Batterieladezustand:", style="My.TLabel"),
            "Stored Battery Energy": ttk.Label(main_frame, text="Gespeicherte Energie der Batterie:", style="My.TLabel"),
            "Battery Power": ttk.Label(main_frame, text="Leistungsabgabe der Batterie:", style="My.TLabel"),
            "Power DC1": ttk.Label(main_frame, text="Leistung Strang 1:", style="My.TLabel"),
            "Power DC2": ttk.Label(main_frame, text="Leistung Strang 2:", style="My.TLabel"),
            "Total Solar Power": ttk.Label(main_frame, text="Gesamte Solarleistung:", style="My.TLabel"),
            "TotalHomeconsumption": ttk.Label(main_frame, text="Gesamter Hausverbrauch:", style="My.TLabel"),
            "PowertoGrid": ttk.Label(main_frame, text="Netzbezug(+), Einspeisung(-):", style="My.TLabel")
        }

        self.value_labels = {
            "Battery temperature": ttk.Label(main_frame, text="0 °C", style="My.TLabel"),
            "Act. state of charge": ttk.Label(main_frame, text="0 %", style="My.TLabel"),
            "Stored Battery Energy": ttk.Label(main_frame, text="0 kWh", style="My.TLabel"),
            "Battery Power": ttk.Label(main_frame, text="0 W", style="My.TLabel"),
            "Power DC1": ttk.Label(main_frame, text="0 W", style="My.TLabel"),
            "Power DC2": ttk.Label(main_frame, text="0 W", style="My.TLabel"),
            "Total Solar Power": ttk.Label(main_frame, text="0 W", style="My.TLabel"),
            "TotalHomeconsumption": ttk.Label(main_frame, text="0 W", style="My.TLabel"),
            "PowertoGrid": ttk.Label(main_frame, text="0 W", style="My.TLabel")
        }

        # Layout
        row = 0
        for key in self.labels:
            self.labels[key].grid(row=row, column=0, sticky=tk.W, pady=8)
            self.value_labels[key].grid(row=row, column=1, sticky=tk.W, padx=15)
            row += 1

        # Hellblauer Block: Netzspannung und Strom der drei Phasen
        phase_frame = ttk.Frame(main_frame, padding="10", style="Blau.TFrame")
        phase_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=8)
        row += 1

        # Kopfzeile
        ttk.Label(phase_frame, text="", style="BlauFett.TLabel").grid(row=0, column=0)
        for col, phase in enumerate(("L1", "L2", "L3"), start=1):
            ttk.Label(phase_frame, text=phase, style="BlauFett.TLabel", width=9,
                      anchor=tk.E).grid(row=0, column=col, padx=5)

        ttk.Label(phase_frame, text="Netzspannung:", style="Blau.TLabel").grid(
            row=1, column=0, sticky=tk.W, pady=4)
        ttk.Label(phase_frame, text="Strom:", style="Blau.TLabel").grid(
            row=2, column=0, sticky=tk.W, pady=4)
        ttk.Label(phase_frame, text="Leistung:", style="Blau.TLabel").grid(
            row=3, column=0, sticky=tk.W, pady=4)

        self.voltage_labels = []
        self.current_labels = []
        self.power_labels = []
        for col in range(1, 4):
            lbl_u = ttk.Label(phase_frame, text="0 V", style="Blau.TLabel", width=9, anchor=tk.E)
            lbl_u.grid(row=1, column=col, padx=5, pady=4)
            self.voltage_labels.append(lbl_u)

            lbl_i = ttk.Label(phase_frame, text="0 A", style="Blau.TLabel", width=9, anchor=tk.E)
            lbl_i.grid(row=2, column=col, padx=5, pady=4)
            self.current_labels.append(lbl_i)

            lbl_p = ttk.Label(phase_frame, text="0 W", style="Blau.TLabel", width=9, anchor=tk.E)
            lbl_p.grid(row=3, column=col, padx=5, pady=4)
            self.power_labels.append(lbl_p)

        # Netzfrequenz
        ttk.Label(phase_frame, text="Netzfrequenz:", style="Blau.TLabel").grid(
            row=4, column=0, sticky=tk.W, pady=4)
        self.frequency_label = ttk.Label(phase_frame, text="0 Hz", style="Blau.TLabel",
                                         anchor=tk.E)
        self.frequency_label.grid(row=4, column=1, columnspan=3, sticky=tk.E, padx=5, pady=4)

        # Status Label
        self.status_label = ttk.Label(main_frame, text="Verbunden", style="My.TLabel")
        self.status_label.grid(row=row, column=0, columnspan=2, pady=8)

    def update_data(self):
        try:
            # Daten vom Kostal Inverter abfragen
            self.kostal.run()

            # Werte aus den Registern holen
            KostalVal = {elements[1]: elements[3] for elements in self.kostal.KostalRegister}

            # Berechnungen
            total_solar_power = round(KostalVal.get("Power DC1", 0) + KostalVal.get("Power DC2", 0), 2)
            battery_power = round(KostalVal.get("Actual battery charge -minus or discharge -plus current", 0) *
                                  KostalVal.get("Battery voltage", 0), 2)
            total_home_consumption = round(
                KostalVal.get("Home own consumption from battery", 0) +
                KostalVal.get("Home own consumption from grid", 0) +
                KostalVal.get("Home own consumption from PV", 0), 1
            )
            power_to_grid = round(
                KostalVal.get("Active power phase 1 (powermeter)", 0) +
                KostalVal.get("Active power phase 2 (powermeter)", 0) +
                KostalVal.get("Active power phase 3 (powermeter)", 0), 1
            )
            # Gespeicherte Energie der Batterie berechnen
            state_of_charge = KostalVal.get("Act. state of charge", 0)
            stored_battery_energy = round(((12.8 / 100) * state_of_charge) - 0.64, 2)

            # Werte aktualisieren
            self.value_labels["Battery temperature"].config(text=f"{KostalVal.get('Battery temperature', 0)} °C")
            self.value_labels["Act. state of charge"].config(text=f"{state_of_charge} %")
            self.value_labels["Stored Battery Energy"].config(text=f"{stored_battery_energy} kWh")
            self.value_labels["Battery Power"].config(
                text=f"{battery_power} W",
                foreground="red" if battery_power > 0 else "black")
            self.value_labels["Power DC1"].config(text=f"{KostalVal.get('Power DC1', 0)} W")
            self.value_labels["Power DC2"].config(text=f"{KostalVal.get('Power DC2', 0)} W")
            self.value_labels["Total Solar Power"].config(text=f"{total_solar_power} W")
            self.value_labels["TotalHomeconsumption"].config(text=f"{total_home_consumption} W")
            self.value_labels["PowertoGrid"].config(text=f"{power_to_grid} W")

            # Spannung und Strom der drei Phasen (Werte des Leistungsmessers am Netzanschluss)
            for i in range(3):
                phase = i + 1
                spannung = KostalVal.get(f"Voltage phase {phase} (powermeter)", 0)
                strom = KostalVal.get(f"Current phase {phase} (powermeter)", 0)
                self.voltage_labels[i].config(text=f"{spannung:.1f} V")
                self.current_labels[i].config(text=f"{strom:.2f} A")
                leistung = KostalVal.get(f"Active power phase {phase} (powermeter)", 0)
                self.power_labels[i].config(text=f"{leistung:.0f} W")

            # Netzfrequenz mit drei Nachkommastellen
            frequenz = KostalVal.get("Grid frequency", 0)
            self.frequency_label.config(text=f"{frequenz:.3f} Hz")

            self.status_label.config(text="Verbunden", foreground="green")

        except Exception as ex:
            self.status_label.config(text=f"Fehler: {str(ex)}", foreground="red")

        # Nächstes Update nach 2 Sekunden planen
        self.root.after(2000, self.update_data)


if __name__ == "__main__":
    root = tk.Tk()
    app = KostalGUI(root)
    root.mainloop()

Installation unter Linux und Windows: Unter Linux, ich benutze Ubuntu MATE, braucht man Python 3, das Paket für Tkinter und pymodbus in der alten Version 2.5.3, weil die Datei noch die ältere Schnittstelle verwendet. Unter Ubuntu installiert man Tkinter mit sudo apt install python3-tk und pymodbus mit pip install pymodbus==2.5.3, gegebenenfalls in einer virtuellen Umgebung oder mit der Option –break-system-packages. Die beiden Dateien legt man in denselben Ordner und startet das Programm mit python3 und dem Dateinamen. Unter Windows funktioniert es genauso, weil Python und Tkinter auf allen Systemen gleich laufen. Man installiert Python von python.org (Tkinter ist dabei), gibt in der Eingabeaufforderung

pip install pymodbus==2.5.3

ein und startet das Programm mit python. Mit pythonw und einer Verknüpfung im Autostart-Ordner läuft es ohne Konsolenfenster beim Anmelden von selbst. Ich habe mit Python 3.10 gearbeitet. Bei sehr neuen Python-Versionen kann pymodbus 2.5.3 Probleme machen, dann muss man die Datei auf die aktuelle pymodbus-Version umstellen.

Vorteile des Programms: Das Programm ist bewusst schlank. Es besteht aus zwei kleinen Dateien, benötigt außer pymodbus nur die Standardbibliothek und belastet Speicher und Prozessor kaum, sodass es rund um die Uhr auf einem alten Laptop oder einem Kleinrechner laufen kann. Der Nutzen zeigt sich im Alltag: Ein Blick genügt, um zu sehen, wie voll die Batterie ist und wie viel die Solaranlage gerade liefert. Danach entscheide ich, ob die Waschmaschine, der Trockner oder ein anderer großer Verbraucher jetzt eingeschaltet wird. Weil alles in Python geschrieben ist, lässt sich das Programm leicht anpassen, und die Abfrage ist rein lesend, sodass am Wechselrichter nichts verstellt wird.

Erweiterungen mit Python: Wie ich in meinem Artikel zur Ansage der Solardaten per Telefon angekündigt habe, ist die Anzeige nur der erste Schritt. Eigentlich geht es darum, große Verbraucher automatisch zu schalten, wenn Strom im Überfluss vorhanden ist, den ich sonst fast verschenken müsste. Ein Beispiel ist ein Heizlüfter an einem WLAN-Zwischenstecker von Shelly, der sich lokal per Netzwerkbefehl schalten lässt. Das Programm schaltet ihn ein, wenn die Batterie fast voll ist und mindestens 1500 Watt eingespeist werden, und wieder aus, sobald die Batterie entladen wird. Sinnvoll sind dabei eine kleine Toleranz und eine Mindestlaufzeit, damit kurze Wolken den Stecker nicht ständig ein- und ausschalten. Ebenso einfach lassen sich die Werte protokollieren, etwa jede Minute eine Zeile mit Zeitstempel in einer CSV-Datei, oder die Erdwärmepumpe über ein Relais steuern, wie im früheren Artikel beschrieben. Auch die Ansage per Telefon mit Asterisk lässt sich mit demselben Modul kombinieren.

Fazit und weiterführende Hinweise: Mit wenig Aufwand entsteht eine Anzeige, die im Alltag wirklich nützt, und ein Grundgerüst für eigene Steuerungen. Den ausführlichen Artikel mit der Einrichtung des Moduls und der Anbindung an Asterisk gibt es unter https://elektronikbasteln.pl7.de/mit-python-und-asterisk-aktuelle-daten-eines-wechselrichter-von-kostal-auslesen,weitere Software für Kostal-Wechselrichter von Kilian Knoll unter https://github.com/kilianknoll. Wie im Kopf der Datei kostal_modbusquery.py steht, gibt es keine Gewähr für die Richtigkeit oder Funktion, die Nutzung erfolgt auf eigenes Risiko.