KSTAR Hybrid-Inverter mit Python und Modbus auslesen

Tags: Python · Modbus · KSTAR · Photovoltaik · Heimenergie · Solarman

Hinweis: KSTAR ist eine Marke des jeweiligen Herstellers. Dieser Beitrag ist unabhängig und nicht vom Hersteller autorisiert oder gesponsert.

KSTAR Modbus Python: Einleitung

Wer einen KSTAR-Hybridwechselrichter im Keller oder am Dach stehen hat, möchte irgendwann wissen, was das Gerät wirklich gerade tut — PV-Leistung, Batterieladung, Netzbezug, Einspeisung. Die Hersteller-App liefert dafür Werte, aber für eigene Auswertungen, Dashboards oder Langzeit-Logging braucht man direkten Datenzugriff.

In diesem Beitrag zeige ich KSTAR Modbus Python in der Praxis: wie ich meinen KSTAR Hybrid-Inverter per Python und Modbus auslese. Technisch läuft die Kommunikation über das Solarman-V5-Protokoll (Modbus-Register über TCP/IP) — dafür eignet sich die Bibliothek pysolarmanv5 sehr gut. Die Register-Definitionen stecken in einer YAML-Datei, das Auslesen übernimmt ein kleines Python-Skript.

KSTAR Modbus Python: Vorbereitung

Benötigte Hardware und Software

  1. KSTAR Hybrid-Inverter mit WLAN-/Ethernet-Datenlogger (Solarman-kompatibel)
  2. Netzwerkverbindung — IP-Adresse und Seriennummer des Datenloggers (stehen in der App oder am Gerät)
  3. Python 3 – Die Programmiersprache für unsere Skripte
  4. pysolarmanv5 und PyYAML — Python-Bibliotheken für Solarman-Kommunikation und Konfiguration

Installation der Abhängigkeiten

pip install pysolarmanv5 pyyaml

KSTAR Modbus Python: Das Skript

Das Skript export.py liest die YAML-Konfiguration ein, holt die Registerblöcke vom Wechselrichter (mit bis zu drei Wiederholversuchen pro Block) und gibt die Werte als pipe-getrennten String aus — praktisch für Cronjobs, Shell-Skripte oder weitere Verarbeitung:

import argparse
from pysolarmanv5 import PySolarmanV5
import yaml
import struct
import datetime
import sys
import time

parser = argparse.ArgumentParser(description='Solarman Script')
parser.add_argument('--debug', type=int, choices=[0, 1], default=0, help='Debug mode (0=off, 1=on)')
args = parser.parse_args()

# Debug-Ausgabe Funktion
def debug_print(message):
    if args.debug == 1:
        print(message)

# Lese die Konfigurationsdatei ein
with open('kstar_hybrid.yaml', 'r') as config_file:
    config = yaml.safe_load(config_file)

# Verbindung zu Solarman herstellen
modbus = PySolarmanV5("192.168.2.150", 1234567890)  # IP und Seriennummer des Datenloggers anpassen

debug_print("")
datum = datetime.datetime.now()
datum_formatiert = datum.strftime("%d.%m.%Y %H:%M:%S")

blocks = []
for request in config['requests']:
    start = request['start']
    end = request['end']
    blocks.append((start, end))
       

debug_print("Register auslesen...")
register_values = {}
for start, end in blocks:
    q = end - start + 1
    
    Versuch = 1    
    while Versuch <= 3:
        try:
            debug_print(f"   .... {start} bis {end} | Versuch: {Versuch} | Anzahl: {q}")
            result = modbus.read_input_registers(register_addr=start, quantity=q)
            if result:
                for i in range(q):
                    register_values[start + i] = result[i]
            break # fertig
        except Exception as e:
            Versuch += 1
            if Versuch <= 3:
                time.sleep(1)   # 1 Sekunde warten...
            else:
                debug_print(f"   Register konnten nicht ausgelesen werden!")
                sys.exit(1)  # Beendet das Skript mit einem Fehlerstatus


datumbis = datetime.datetime.now()
datumbis_formatiert = datumbis.strftime("%d.%m.%Y %H:%M:%S")
datediff = datumbis - datum
total_seconds = round(datediff.total_seconds(),0)
debug_print(f"Register ausgelesen ({total_seconds}s)")

debug_print("")
debug_print("Parameter auswerten...")


# Funktion zum Auslesen von 16-Bit-Registern und Zusammenfassen zu einem Wert mit umgekehrter Wertigkeit
def read_and_combine_registers(register_list):
    combined_value = 0
    for register in reversed(register_list):  # Umkehrung der Reihenfolge der Register
        if register in register_values:
            combined_value <<= 16
            combined_value |= register_values[register]
    return combined_value

# Funktion zum Runden eines Werts basierend auf der scale-Einstellung
def round_value(value, scale):
    if scale == 0.1:
        return round(value, 1)
    elif scale == 0.01:
        return round(value, 2)
    elif scale == 0.001:
        return round(value, 3)
    else:
        return value

# Funktion zum Konvertieren des Werts anhand der lookup-Tabelle
def convert_with_lookup(value, lookup_table):
    for entry in lookup_table:
        if entry['key'] == value:
            return entry['value']
    return value

# Funktion zum Konvertieren von Byte-Folgen in Strings mit UTF-8-Kodierung oder ASCII-Zeichen
def convert_bytes_to_string(register_list):
    byte_list = []
    for register in register_list:
        if register in register_values:
            # Extrahiere die beiden Bytes aus dem Register und füge sie zur Liste hinzu
            byte_list.extend(register_values[register].to_bytes(2, byteorder='big'))
    
    try:
        # Versuche, die Byte-Liste als UTF-8-kodierten String zu dekodieren
        string_value = bytes(byte_list).decode('utf-8')
    except UnicodeDecodeError:
        # Im Falle eines Decoding-Fehlers gib die Bytes als ASCII-Zeichen aus
        string_value = 'unbekannt'
    
    return string_value


# Iteriere über die Konfigurationsparameter
export = f"Export.Datum={datum_formatiert}|"
for parameter in config['parameters']:
    if 'group' in parameter and 'items' in parameter:
        group = parameter['group']
        items = parameter['items']

        for item in items:            
            if 'name' in item and 'registers' in item and 'scale' in item:
                name = item['name']
                registers = item['registers']
                scale = item['scale']

                export_value = ""
                if 'rule' in item and item['rule'] == 5:
                    # Wenn rule auf 5 gesetzt ist, handelt es sich um eine Byte-Folge, die einen String darstellt
                    string_value = convert_bytes_to_string(registers)
                    export_value = string_value
                else:
                    # Andernfalls handelt es sich um einen numerischen Wert
                    # Lese die Registerwerte aus und kombiniere sie zu einem Wert mit umgekehrter Wertigkeit
                    combined_value = read_and_combine_registers(registers)

                    # Berücksichtige den Scale-Faktor und runde den Wert entsprechend
                    scaled_value = round_value(combined_value * scale, scale)

                    # Wenn eine lookup-Tabelle definiert ist, konvertiere den Wert entsprechend
                    if 'lookup' in item:
                        converted_value = convert_with_lookup(scaled_value, item['lookup'])
                    else:
                        converted_value = scaled_value
                    export_value = converted_value
                
                s = f"{group}.{name}={export_value}" 
                export += s + "|"
                
                debug_print(f"   {s}")


export += f"Export.Enddatum={datumbis_formatiert}|"

debug_print("Parameter ausgewertet")

if args.debug == 0:
    print(export)

Die Konfiguration

Im Zentrum steht kstar_hybrid.yaml: Sie definiert, welche Modbus-Register in welchen Blöcken abgefragt werden und wie die Rohwerte in lesbare Größen umgerechnet werden. Die Register-Angaben basieren auf dem öffentlich verfügbaren Protokolldokument des Herstellers; im Blog zeige ich nur einen Auszug — die vollständige Datei umfasst alle PV-, Netz-, Batterie- und Phasenwerte:

# KSTAR Hybrid Inverter
# Register laut KSTAR „MODBUS RS485 Communication Protocol V2.5“
#
# Input-Register:  3000–3660
# Holding-Register: 3200–3237
# Pro Modbus-Request max. 125 Register → Anfragen aufteilen.

requests:
  - start: 3000
    end: 3124
    mb_functioncode: 0x04
  - start: 3125
    end: 3199
    mb_functioncode: 0x04
  - start: 3200
    end: 3217
    mb_functioncode: 0x03   # Holding-Register (Systeminfos)
  - start: 3228
    end: 3249
    mb_functioncode: 0x04
  - start: 3250
    end: 3301
    mb_functioncode: 0x04

parameters:
  - group: solar
    items:
      - name: "PV1 Voltage"
        scale: 0.1
        rule: 1
        registers: [3000]
      - name: "PV1 Power"
        scale: 1
        rule: 2
        registers: [3024]
      - name: "Daily Production"
        scale: 0.1
        rule: 1
        registers: [3036]

  - group: Battery
    items:
      - name: "Battery SoC"
        scale: 0.1
        rule: 1
        registers: [3066]

  - group: Power Grid
    items:
      - name: "Daily Energy Feed-In"
        scale: 0.1
        rule: 1
        registers: [3116]

# … weitere Gruppen (Verbrauch, Phasen, Inverter-Status) analog definiert.
# rule: 1 = 16-Bit-Wert, 2 = vorzeichenbehaftet, 3 = 32-Bit (2 Register),
#       5 = String aus Byte-Folge, 6 = Bitfeld

Ausgabeformat

Ohne --debug gibt das Skript eine einzeilige Ausgabe aus — Gruppe, Name und Wert durch Pipes getrennt:

Export.Datum=12.07.2026 10:15:00|solar.PV1 Voltage=312.4|solar.PV1 Power=1842|Battery.Battery SoC=78.5|Power Grid.Daily Energy Feed-In=12.3|Export.Enddatum=12.07.2026 10:15:04|

Mit python3 export.py --debug 1 sieht man zusätzlich, welche Registerblöcke gelesen werden und wie lange das dauert — hilfreich beim ersten Test.

Automatisierung mit Shell-Skripten

Für regelmäßiges Auslesen eignet sich ein Cronjob mit einem kleinen Shell-Skript. Es schreibt die Ausgabe atomar in eine HTML-Datei (nur wenn das Skript tatsächlich Daten geliefert hat):

#!/bin/bash

cd /home/ubuntu/python

# Führe das Python-Skript aus und speichere die Ausgabe
python3 export.py > /var/www/html/export_neu.html

# Überprüfe, ob die Datei nicht leer ist
if [ -s /var/www/html/export_neu.html ]; then
    cp /var/www/html/export_neu.html /var/www/html/export.html
    echo "Die Datei export.html wurde erfolgreich aktualisiert."
else
    echo "Das Python-Skript hat kein Ergebnis zurückgegeben."
fi

Monitoring und Visualisierung

Die pipe-getrennte Ausgabe lässt sich leicht weiterverarbeiten — zum Beispiel in CSV schreiben, in eine Datenbank importieren oder an Home Assistant anbinden. Typische Auswertungen:

  1. Echtzeit-Dashboard: Aktuelle Leistungswerte
  2. Historische Analysen: Energiebilanz über Monate/Jahre
  3. Effizienzberichte: Vergleich von Produktion und Verbrauch
  4. Wartungsindikatoren: Batteriezustand und Temperaturen

Sicherheitshinweise

  • Stellen Sie sicher, dass der Modbus-Zugang nur aus vertrauenswürdigen Netzwerken möglich ist
  • Verwenden Sie starke Passwörter für den Inverter-Zugang
  • Regelmäßige Firmware-Updates nicht vergessen
  • Sensible Daten verschlüsselt speichern

Fazit

Der Ansatz ist bewusst schlicht gehalten: YAML beschreibt die Register, Python liest sie aus, Shell oder Cronjob sorgt für regelmäßige Updates. Kein schwerer Stack, keine Cloud-Pflicht — aber volle Kontrolle über die eigenen Energiedaten. Wenn du ähnliche Wechselrichter hast, lässt sich das Muster oft übernehmen; entscheidend ist die passende Register-Map für dein Modell.

Mehr zu Modbus im Blog: KSTAR Modbus Python und Carlo Gavazzi Modbus TCP.