NEWS
Sofar Solar HYD10 KTL Wechselrichter an modbus Adapter
-
https://www.solarvie.at/en-eu/products/sofarsolar-ethernet-dongle-lse-3
kaufe dir einfach dieses Teil und du hast Ruhe, 55 Euro kostet weniger als die Nerven die du lassen musst. Vor allem hast du es gleich auch noch in der App
-
Habe mir eine eigene Lösung geschrieben, welche ich euch nicht vorenthalten möchte.
Habe in einem LXC, welcher für diverse Skripte zuständig ist, folgendes Python Script drauf gemacht plus eine config.yaml .
Dies sendet die Sofar-Daten per MQTT an den ioBroker./opt/modbus-mqtt/modbus_tcp_rtu.py
#!/usr/bin/env python3 import socket import struct import time import yaml import logging import paho.mqtt.client as mqtt import os import re CONFIG_FILE = "/opt/modbus-mqtt/config.yaml" LOG_FILE = "/var/log/modbus-mqtt.log" logging.basicConfig( filename=LOG_FILE, level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s" ) logging.info("Starting modbus-mqtt with config from disk") try: with open(CONFIG_FILE, "r") as f: cfg = yaml.safe_load(f) except Exception as e: logging.error(f"Failed to load config: {e}") exit(1) last_mtime = os.path.getmtime(CONFIG_FILE) MODBUS_HOST = cfg["modbus"]["host"] MODBUS_PORT = cfg["modbus"]["port"] UNIT_ID = cfg["modbus"]["unit_id"] MQTT_HOST = cfg["mqtt"]["host"] MQTT_PORT = cfg["mqtt"]["port"] BASE_TOPIC = cfg["mqtt"]["base_topic"] POLL_INTERVAL = cfg["poll_interval"] REGISTERS = cfg["registers"] mqttc = mqtt.Client(client_id="modbus_mqtt_bridge", clean_session=True) mqtt_user = cfg["mqtt"].get("username") mqtt_pass = cfg["mqtt"].get("password") if mqtt_user and mqtt_pass: mqttc.username_pw_set(mqtt_user, mqtt_pass) elif mqtt_user or mqtt_pass: logging.warning("MQTT username or password missing. Authentication disabled.") else: logging.info("No auth data found. Start without authentication") mqttc.connect(MQTT_HOST, MQTT_PORT) mqttc.loop_start() # --------------------------------------------------------- # Text bereinigen # --------------------------------------------------------- def sanitize(text): # Umlaute ersetzen text = ( text.replace("ä", "ae") .replace("ö", "oe") .replace("ü", "ue") .replace("Ä", "Ae") .replace("Ö", "Oe") .replace("Ü", "Ue") .replace("ß", "ss") ) # Problematische Zeichen ersetzen text = text.replace("-", "_") text = text.replace(":", "_") # Erlaubt: A-Z, a-z, 0-9, _ text = re.sub(r"[^A-Za-z0-9_]+", "_", text) # Mehrere Unterstriche zu einem machen text = re.sub(r"_+", "_", text) # Anfang/Ende säubern return text.strip("_") # --------------------------------------------------------- # CRC16 Modbus # --------------------------------------------------------- def crc16(data): crc = 0xFFFF for pos in data: crc ^= pos for _ in range(8): if crc & 1: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return crc.to_bytes(2, byteorder="little") # --------------------------------------------------------- # RTU‑Frame senden über TCP # --------------------------------------------------------- def read_holding_registers_rtu_tcp(unit, address, count): # Build RTU frame: [unit][function][addr_hi][addr_lo][count_hi][count_lo][crc_lo][crc_hi] frame = bytearray() frame.append(unit) frame.append(0x03) # Function code: Read Holding Registers frame += struct.pack(">HH", address, count) frame += crc16(frame) # TCP socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(2) s.connect((MODBUS_HOST, MODBUS_PORT)) s.send(frame) # Response response = s.recv(256) s.close() # Validate minimum length if len(response) < 5: raise Exception("Invalid response length") # Validate CRC data = response[:-2] crc_received = response[-2:] crc_calc = crc16(data) if crc_received != crc_calc: raise Exception("CRC mismatch") # Byte count byte_count = response[2] if byte_count != count * 2: raise Exception("Unexpected byte count") # Extract registers registers = [] for i in range(count): hi = response[3 + i*2] lo = response[4 + i*2] registers.append((hi << 8) | lo) return registers # --------------------------------------------------------- # Poll‑Loop # --------------------------------------------------------- last_values = {} while True: # Auto‑Reload wenn config.yaml geändert wurde current_mtime = os.path.getmtime(CONFIG_FILE) if current_mtime != last_mtime: logging.info("Config changed, restarting...") mqttc.loop_stop() exit(0) for reg in REGISTERS: addr = reg["addr"] reg_type = reg["type"] factor = reg["factor"] length = 2 if reg_type == "uint32" else 1 try: regs = read_holding_registers_rtu_tcp(UNIT_ID, addr, length) if reg_type == "int16": value = struct.unpack(">h", struct.pack(">H", regs[0]))[0] elif reg_type == "uint16": value = regs[0] elif reg_type == "uint32": value = (regs[0] << 16) | regs[1] else: continue value *= factor # Nachkommastellen basierend auf config.yaml decimals = reg.get("decimals", None) if decimals is not None: value = round(value, decimals) # Beschreibung + Name in Topic einbauen name = reg.get("name", "") desc = reg.get("description", "") name_clean = sanitize(name) desc_clean = sanitize(desc) # Format: addr:name-description topic_suffix = str(addr) if name_clean: topic_suffix += f"_{name_clean}" if desc_clean: topic_suffix += f"_{desc_clean}" topic = f"{BASE_TOPIC}/register/{topic_suffix}" # Nur senden, wenn sich der Wert geändert hat if addr in last_values and last_values[addr] == value: continue # Wert unverändert → nicht senden last_values[addr] = value mqttc.publish(topic, value) #logging.info(f"Published {topic} = {value}") except Exception as e: logging.error(f"Error reading register {addr}: {e}") time.sleep(0.2) time.sleep(POLL_INTERVAL)/opt/modbus-mqtt/config.yaml
modbus: host: "192.168.88.101" port: 8899 unit_id: 1 mqtt: host: "192.168.1.251" port: 1883 base_topic: "modbus/inverter" username: "mqtt_user" password: "mqtt_passwort" poll_interval: 13 registers: - addr: 1199 # Total Verbrauch in kW name: "ActivePower_Load_Sys" type: "uint16" factor: 0.01 decimals: 2 description: "Total Verbrauch" - addr: 1160 # BKW Netz in kW name: "ActivePower_PCC_Total" type: "int16" factor: 0.01 decimals: 2 description: "BKW Netz" - addr: 1476 # PV Produktion in kW name: "Power_PV_Total" type: "uint16" factor: 0.1 decimals: 2 description: "PV Produktion" - addr: 1542 # Akku Leistung in kW name: "Power_Bat1" type: "int16" factor: 0.01 decimals: 2 description: "Akku Leistung" - addr: 1544 # Akku Ladung in % name: "SOC_Bat1" type: "uint16" factor: 1 decimals: 0 description: "Akku Ladung" - addr: 1668 # PV Produktion heute in kWh name: "PV_Generation_Today" type: "uint32" factor: 0.01 decimals: 2 description: "PV Produktion heute"Beim Waveshare musste ich auf "Transparent" (anstelle von "modbus TCP <=> modbus RTU") stellen.
In der config seht ihr auch, dass LXCs und der Waveshare nicht im gleichen Netzwerk sind. WAN von 192.168.88.0 (MikroTik Router) hängt am Netz 192.168.1.0 (Fritzbox). Da hab ich eine Route definiert, damit man vom einen ins andere Netz zugreifen kann.
Die IPs, Ports, Usernames und Passwörter müsst ihr natürlich auf eure Gegebenheiten anpassen.In der /etc/systemd/system/modbus-mqtt.service
[Unit] Description=Modbus RTU over TCP to MQTT Gateway After=network-online.target Wants=network-online.target [Service] Type=simple User=root WorkingDirectory=/opt/modbus-mqtt ExecStart=/usr/bin/env python3 /opt/modbus-mqtt/modbus_tcp_rtu.py Restart=always RestartSec=5 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.targetWenn alles fertig ist, eingeben:
sudo systemctl daemon-reload sudo systemctl enable modbus-mqtt.service sudo systemctl start modbus-mqtt.serviceDie Daten werden nun per MQTT an den ioBroker gesendet.
Der Datenpunkt sieht dann etwa so aus:mqtt.0.modbus.inverter.register.{Registeradresse}

-
Habe mir eine eigene Lösung geschrieben, welche ich euch nicht vorenthalten möchte.
Habe in einem LXC, welcher für diverse Skripte zuständig ist, folgendes Python Script drauf gemacht plus eine config.yaml .
Dies sendet die Sofar-Daten per MQTT an den ioBroker./opt/modbus-mqtt/modbus_tcp_rtu.py
#!/usr/bin/env python3 import socket import struct import time import yaml import logging import paho.mqtt.client as mqtt import os import re CONFIG_FILE = "/opt/modbus-mqtt/config.yaml" LOG_FILE = "/var/log/modbus-mqtt.log" logging.basicConfig( filename=LOG_FILE, level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s" ) logging.info("Starting modbus-mqtt with config from disk") try: with open(CONFIG_FILE, "r") as f: cfg = yaml.safe_load(f) except Exception as e: logging.error(f"Failed to load config: {e}") exit(1) last_mtime = os.path.getmtime(CONFIG_FILE) MODBUS_HOST = cfg["modbus"]["host"] MODBUS_PORT = cfg["modbus"]["port"] UNIT_ID = cfg["modbus"]["unit_id"] MQTT_HOST = cfg["mqtt"]["host"] MQTT_PORT = cfg["mqtt"]["port"] BASE_TOPIC = cfg["mqtt"]["base_topic"] POLL_INTERVAL = cfg["poll_interval"] REGISTERS = cfg["registers"] mqttc = mqtt.Client(client_id="modbus_mqtt_bridge", clean_session=True) mqtt_user = cfg["mqtt"].get("username") mqtt_pass = cfg["mqtt"].get("password") if mqtt_user and mqtt_pass: mqttc.username_pw_set(mqtt_user, mqtt_pass) elif mqtt_user or mqtt_pass: logging.warning("MQTT username or password missing. Authentication disabled.") else: logging.info("No auth data found. Start without authentication") mqttc.connect(MQTT_HOST, MQTT_PORT) mqttc.loop_start() # --------------------------------------------------------- # Text bereinigen # --------------------------------------------------------- def sanitize(text): # Umlaute ersetzen text = ( text.replace("ä", "ae") .replace("ö", "oe") .replace("ü", "ue") .replace("Ä", "Ae") .replace("Ö", "Oe") .replace("Ü", "Ue") .replace("ß", "ss") ) # Problematische Zeichen ersetzen text = text.replace("-", "_") text = text.replace(":", "_") # Erlaubt: A-Z, a-z, 0-9, _ text = re.sub(r"[^A-Za-z0-9_]+", "_", text) # Mehrere Unterstriche zu einem machen text = re.sub(r"_+", "_", text) # Anfang/Ende säubern return text.strip("_") # --------------------------------------------------------- # CRC16 Modbus # --------------------------------------------------------- def crc16(data): crc = 0xFFFF for pos in data: crc ^= pos for _ in range(8): if crc & 1: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return crc.to_bytes(2, byteorder="little") # --------------------------------------------------------- # RTU‑Frame senden über TCP # --------------------------------------------------------- def read_holding_registers_rtu_tcp(unit, address, count): # Build RTU frame: [unit][function][addr_hi][addr_lo][count_hi][count_lo][crc_lo][crc_hi] frame = bytearray() frame.append(unit) frame.append(0x03) # Function code: Read Holding Registers frame += struct.pack(">HH", address, count) frame += crc16(frame) # TCP socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(2) s.connect((MODBUS_HOST, MODBUS_PORT)) s.send(frame) # Response response = s.recv(256) s.close() # Validate minimum length if len(response) < 5: raise Exception("Invalid response length") # Validate CRC data = response[:-2] crc_received = response[-2:] crc_calc = crc16(data) if crc_received != crc_calc: raise Exception("CRC mismatch") # Byte count byte_count = response[2] if byte_count != count * 2: raise Exception("Unexpected byte count") # Extract registers registers = [] for i in range(count): hi = response[3 + i*2] lo = response[4 + i*2] registers.append((hi << 8) | lo) return registers # --------------------------------------------------------- # Poll‑Loop # --------------------------------------------------------- last_values = {} while True: # Auto‑Reload wenn config.yaml geändert wurde current_mtime = os.path.getmtime(CONFIG_FILE) if current_mtime != last_mtime: logging.info("Config changed, restarting...") mqttc.loop_stop() exit(0) for reg in REGISTERS: addr = reg["addr"] reg_type = reg["type"] factor = reg["factor"] length = 2 if reg_type == "uint32" else 1 try: regs = read_holding_registers_rtu_tcp(UNIT_ID, addr, length) if reg_type == "int16": value = struct.unpack(">h", struct.pack(">H", regs[0]))[0] elif reg_type == "uint16": value = regs[0] elif reg_type == "uint32": value = (regs[0] << 16) | regs[1] else: continue value *= factor # Nachkommastellen basierend auf config.yaml decimals = reg.get("decimals", None) if decimals is not None: value = round(value, decimals) # Beschreibung + Name in Topic einbauen name = reg.get("name", "") desc = reg.get("description", "") name_clean = sanitize(name) desc_clean = sanitize(desc) # Format: addr:name-description topic_suffix = str(addr) if name_clean: topic_suffix += f"_{name_clean}" if desc_clean: topic_suffix += f"_{desc_clean}" topic = f"{BASE_TOPIC}/register/{topic_suffix}" # Nur senden, wenn sich der Wert geändert hat if addr in last_values and last_values[addr] == value: continue # Wert unverändert → nicht senden last_values[addr] = value mqttc.publish(topic, value) #logging.info(f"Published {topic} = {value}") except Exception as e: logging.error(f"Error reading register {addr}: {e}") time.sleep(0.2) time.sleep(POLL_INTERVAL)/opt/modbus-mqtt/config.yaml
modbus: host: "192.168.88.101" port: 8899 unit_id: 1 mqtt: host: "192.168.1.251" port: 1883 base_topic: "modbus/inverter" username: "mqtt_user" password: "mqtt_passwort" poll_interval: 13 registers: - addr: 1199 # Total Verbrauch in kW name: "ActivePower_Load_Sys" type: "uint16" factor: 0.01 decimals: 2 description: "Total Verbrauch" - addr: 1160 # BKW Netz in kW name: "ActivePower_PCC_Total" type: "int16" factor: 0.01 decimals: 2 description: "BKW Netz" - addr: 1476 # PV Produktion in kW name: "Power_PV_Total" type: "uint16" factor: 0.1 decimals: 2 description: "PV Produktion" - addr: 1542 # Akku Leistung in kW name: "Power_Bat1" type: "int16" factor: 0.01 decimals: 2 description: "Akku Leistung" - addr: 1544 # Akku Ladung in % name: "SOC_Bat1" type: "uint16" factor: 1 decimals: 0 description: "Akku Ladung" - addr: 1668 # PV Produktion heute in kWh name: "PV_Generation_Today" type: "uint32" factor: 0.01 decimals: 2 description: "PV Produktion heute"Beim Waveshare musste ich auf "Transparent" (anstelle von "modbus TCP <=> modbus RTU") stellen.
In der config seht ihr auch, dass LXCs und der Waveshare nicht im gleichen Netzwerk sind. WAN von 192.168.88.0 (MikroTik Router) hängt am Netz 192.168.1.0 (Fritzbox). Da hab ich eine Route definiert, damit man vom einen ins andere Netz zugreifen kann.
Die IPs, Ports, Usernames und Passwörter müsst ihr natürlich auf eure Gegebenheiten anpassen.In der /etc/systemd/system/modbus-mqtt.service
[Unit] Description=Modbus RTU over TCP to MQTT Gateway After=network-online.target Wants=network-online.target [Service] Type=simple User=root WorkingDirectory=/opt/modbus-mqtt ExecStart=/usr/bin/env python3 /opt/modbus-mqtt/modbus_tcp_rtu.py Restart=always RestartSec=5 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.targetWenn alles fertig ist, eingeben:
sudo systemctl daemon-reload sudo systemctl enable modbus-mqtt.service sudo systemctl start modbus-mqtt.serviceDie Daten werden nun per MQTT an den ioBroker gesendet.
Der Datenpunkt sieht dann etwa so aus:mqtt.0.modbus.inverter.register.{Registeradresse}

@spicer sagte in Sofar Solar HYD10 KTL Wechselrichter an modbus Adapter:
Beim Waveshare musste ich auf "Transparent" (anstelle von "modbus TCP <=> modbus RTU") stellen.
das ist, warum mir ein solcher Konverter zu komplex ist.
Dann noch bei gleichem Hersteller unterschiedliche Einstellungen braucht....@spicer sagte in Sofar Solar HYD10 KTL Wechselrichter an modbus Adapter:
dass LXCs und der Waveshare nicht im gleichen Netzwerk sind.
davon war bisher keine Rede
-
@spicer sagte in Sofar Solar HYD10 KTL Wechselrichter an modbus Adapter:
Beim Waveshare musste ich auf "Transparent" (anstelle von "modbus TCP <=> modbus RTU") stellen.
das ist, warum mir ein solcher Konverter zu komplex ist.
Dann noch bei gleichem Hersteller unterschiedliche Einstellungen braucht....@spicer sagte in Sofar Solar HYD10 KTL Wechselrichter an modbus Adapter:
dass LXCs und der Waveshare nicht im gleichen Netzwerk sind.
davon war bisher keine Rede
-
@Homoran sagte in Sofar Solar HYD10 KTL Wechselrichter an modbus Adapter:
davon war bisher keine Rede
Das hat ja auch nichts mit den "Hängern" zutun ;)
Der Adapter läuft ja ein paar Stunden gut.@spicer sagte in Sofar Solar HYD10 KTL Wechselrichter an modbus Adapter:
Das hat ja auch nichts mit den "Hängern" zutun ;)
sicher?
der socket wird angemeckert!
-
@spicer sagte in Sofar Solar HYD10 KTL Wechselrichter an modbus Adapter:
Das hat ja auch nichts mit den "Hängern" zutun ;)
sicher?
der socket wird angemeckert!
@Homoran sagte in Sofar Solar HYD10 KTL Wechselrichter an modbus Adapter:
@spicer sagte in Sofar Solar HYD10 KTL Wechselrichter an modbus Adapter:
Das hat ja auch nichts mit den "Hängern" zutun ;)
sicher?
der socket wird angemeckert!
Ja eben. Das hab ich weiter oben ja schon festgehalten.
Da konnte mir niemand einen Rat geben.
Darum hab ich dann ein eigenes Script erstellt ;)
Und das läuft seit der Erstellung stabil. -
dem HYD in 10Sek auszulesen ist keine gute Idee
ich habe auch hin und wieder Fehler, aber er liest bei mir danach einfach weiter aus.weiterhin verwendest du einen Waveshare, was ich nicht ganz verstehe, warum du da immer noch Port 8899 drin hast im Modbus Adapter vom IOBroker
ich musste den Port vom Waveshare im Modbus vom IOB sowie die IP vom Waveshare da eintragen und im Waveshare den Port vom WR
so wundert mich das.ich nutze einen Waveshare 4CH und das läuft echt sauber und problemlos
1x Sofar HYD
1x Sofar KTLX
1x Growatt
gleichzeitig mit vers. Auslesezeiten und mit 3 Modbus Adaptern
vorher einen einfachen Waveshare nur für den HYD, lief auch stabil -
dem HYD in 10Sek auszulesen ist keine gute Idee
ich habe auch hin und wieder Fehler, aber er liest bei mir danach einfach weiter aus.weiterhin verwendest du einen Waveshare, was ich nicht ganz verstehe, warum du da immer noch Port 8899 drin hast im Modbus Adapter vom IOBroker
ich musste den Port vom Waveshare im Modbus vom IOB sowie die IP vom Waveshare da eintragen und im Waveshare den Port vom WR
so wundert mich das.ich nutze einen Waveshare 4CH und das läuft echt sauber und problemlos
1x Sofar HYD
1x Sofar KTLX
1x Growatt
gleichzeitig mit vers. Auslesezeiten und mit 3 Modbus Adaptern
vorher einen einfachen Waveshare nur für den HYD, lief auch stabil@Maddin77 sagte in Sofar Solar HYD10 KTL Wechselrichter an modbus Adapter:
dem HYD in 10Sek auszulesen ist keine gute Idee
ich habe auch hin und wieder Fehler, aber er liest bei mir danach einfach weiter aus.weiterhin verwendest du einen Waveshare, was ich nicht ganz verstehe, warum du da immer noch Port 8899 drin hast im Modbus Adapter vom IOBroker
ich musste den Port vom Waveshare im Modbus vom IOB sowie die IP vom Waveshare da eintragen und im Waveshare den Port vom WR
so wundert mich das.ich nutze einen Waveshare 4CH und das läuft echt sauber und problemlos
1x Sofar HYD
1x Sofar KTLX
1x Growatt
gleichzeitig mit vers. Auslesezeiten und mit 3 Modbus Adaptern
vorher einen einfachen Waveshare nur für den HYD, lief auch stabilIch habe nicht den gleichen Waveshare (siehe oben).
Hatte auf 20s eingestellt. Trotzdem standen die DPs plötzlich still.
Mit meinem Script läuft es nun stabil. Habe da sogar 13s Intervall.