diff --git a/loadvars.sh b/loadvars.sh index 94f3c574c..9f434928a 100755 --- a/loadvars.sh +++ b/loadvars.sh @@ -1090,7 +1090,7 @@ loadvars(){ fi echo $hausverbrauch > /var/www/html/openWB/ramdisk/hausverbrauch usesimbezug=0 - if [[ $wattbezugmodul == "bezug_solarwatt" ]]|| [[ $wattbezugmodul == "bezug_rct" ]]|| [[ $wattbezugmodul == "bezug_varta" ]] || [[ $wattbezugmodul == "bezug_kostalplenticoreem300haus" ]] || [[ $wattbezugmodul == "bezug_solarlog" ]] ; then + if [[ $wattbezugmodul == "bezug_solarwatt" ]]|| [[ $wattbezugmodul == "bezug_rct" ]]|| [[ $wattbezugmodul == "bezug_varta" ]] || [[ $wattbezugmodul == "bezug_solarlog" ]] ; then usesimbezug=1 fi if ((usesimbezug == 1)); then @@ -1129,9 +1129,6 @@ loadvars(){ fi usesimpv=0 - if [[ $speichermodul == "speicher_kostalplenticore" ]] && [[ $pvwattmodul == "wr_plenticore" ]]; then - usesimpv=1 - fi if [[ $pvwattmodul == "wr_rct" ]]|| [[ $pvwattmodul == "wr_solarwatt" ]] || [[ $pvwattmodul == "wr_shelly" ]] || [[ $pvwattmodul == "wr_kostalpikovar2" ]]; then usesimpv=1 fi @@ -1218,7 +1215,7 @@ loadvars(){ echo "$pvallwh" > /var/www/html/openWB/ramdisk/pvallwh fi - if [[ $speichermodul == "speicher_tesvoltsma" ]] || [[ $speichermodul == "speicher_solarwatt" ]] || [[ $speichermodul == "speicher_rct" ]]|| [[ $speichermodul == "speicher_kostalplenticore" ]] || [[ $speichermodul == "speicher_varta" ]] ; then + if [[ $speichermodul == "speicher_tesvoltsma" ]] || [[ $speichermodul == "speicher_solarwatt" ]] || [[ $speichermodul == "speicher_rct" ]] || [[ $speichermodul == "speicher_varta" ]] ; then ra='^-?[0-9]+$' watt2=$(>"${MYLOGFILE}" 2>&1 -ret=$? - -openwbDebugLog ${DMOD} 2 "RET: ${ret}" +bash "$OPENWBBASEDIR/packages/legacy_run.sh" "modules.kostal_plenticore.device" "counter" "${kostalplenticoreip}" "" "0" "" "${kostalplenticorehaus}" >> "$MYLOGFILE" 2>&1 cat "${RAMDISKDIR}/wattbezug" diff --git a/modules/speicher_kostalplenticore/kostal_plenticore.py b/modules/speicher_kostalplenticore/kostal_plenticore.py deleted file mode 100755 index 50306921b..000000000 --- a/modules/speicher_kostalplenticore/kostal_plenticore.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 - -######################################################### -# -# liest aus Wechselrichter Kostal Plenticore -# mit angeschlossener Batterie die Lade-/Entladeleistung -# und den Batterie-SOC -# -# 2019 Michael Ortenstein -# This file is part of openWB -# -######################################################### - -# Daten aus temporärer ramdisk zur globalen Weiterverarbeitung in die -# entsprechenden ramdisks kopieren. Die temporären Werte stammen aus dem -# wr_plenticore Modul, werden dort zentral aus den Modbus-Registern gelesen - -from typing import List -import shutil - -from helpermodules.cli import run_using_positional_cli_args - - -def update(): - # Speicherleistung WR 1 - shutil.copy("/var/www/html/openWB/ramdisk/temp_speicherleistung", "/var/www/html/openWB/ramdisk/speicherleistung") - # Speicher Ladestand von Speicher am WR 1 - shutil.copy("/var/www/html/openWB/ramdisk/temp_speichersoc", "/var/www/html/openWB/ramdisk/speichersoc") - - -def main(argv: List[str]): - run_using_positional_cli_args(update, argv) diff --git a/modules/speicher_kostalplenticore/main.sh b/modules/speicher_kostalplenticore/main.sh index 425dc3b73..c55416821 100755 --- a/modules/speicher_kostalplenticore/main.sh +++ b/modules/speicher_kostalplenticore/main.sh @@ -1,19 +1,5 @@ #!/bin/bash -OPENWBBASEDIR=$(cd "$(dirname "$0")/../../" && pwd) -RAMDISKDIR="${OPENWBBASEDIR}/ramdisk" -#DMOD="BATT" -DMOD="MAIN" - -if [ ${DMOD} == "MAIN" ]; then - MYLOGFILE="${RAMDISKDIR}/openWB.log" -else - MYLOGFILE="${RAMDISKDIR}/speicher.log" -fi - -bash "$OPENWBBASEDIR/packages/legacy_run.sh" "speicher_kostalplenticore.kostal_plenticore" >>"${MYLOGFILE}" 2>&1 -ret=$? - -openwbDebugLog ${DMOD} 2 "RET: ${ret}" -speicherleistung=$(<"${RAMDISKDIR}/speicherleistung") -openwbDebugLog ${DMOD} 1 "BattLeistung: ${speicherleistung}" +# wr_plenticore will fetch data for both PV and battery and there is nothing left for us to do. +# +# The usage of speicher_kostalplenticore without wr_plenticore is not intended and thus not handled. diff --git a/modules/wr_plenticore/main.sh b/modules/wr_plenticore/main.sh index 3b9bdf73c..777164f63 100755 --- a/modules/wr_plenticore/main.sh +++ b/modules/wr_plenticore/main.sh @@ -31,7 +31,7 @@ else MYLOGFILE="${RAMDISKDIR}/nurpv.log" fi -bash "$OPENWBBASEDIR/packages/legacy_run.sh" "wr_plenticore.read_kostalplenticore" "${kostalplenticoreip}" "${kostalplenticoreip2}" "${Battery}" "${kostalplenticoreip3}" >>"${MYLOGFILE}" 2>&1 +bash "$OPENWBBASEDIR/packages/legacy_run.sh" "modules.kostal_plenticore.device" "inverter" "${kostalplenticoreip}" "${kostalplenticoreip2}" "${Battery}" "${kostalplenticoreip3}">>"${MYLOGFILE}" 2>&1 ret=$? openwbDebugLog ${DMOD} 2 "RET: ${ret}" diff --git a/modules/wr_plenticore/read_kostalplenticore.py b/modules/wr_plenticore/read_kostalplenticore.py deleted file mode 100755 index 792e39332..000000000 --- a/modules/wr_plenticore/read_kostalplenticore.py +++ /dev/null @@ -1,630 +0,0 @@ -#!/usr/bin/python -# coding: utf8 - -######################################################### -# -# liest aus Wechselrichter Kostal Plenticore Register -# zu PV-Statistik und berechnet PV-Leistung, Speicherleistung -# unter Beachtung angeschlossener Batterie falls vorhanden -# -# WICHTIG: Tagesertrag wird nicht ausgelesen, dieser wird durch openWB berechnet! -# Kostal sieht Ertrag erst, wenn DC-AC-Wandlung erfolgte. Somit entsteht Ertrag in diesem Sinne auch, -# wenn der Speicher Leistung abgibt. PV-Leistung, die in den Speicher geht, sieht Kostal nicht als -# Ertrag. Da openWB den Tagesertrag jedoch als PV-Ertrag interpretiert (also ges. Energiemenge, -# die von der PV erzeugt wurde, einschließlich Speicherladung), berechnet openWB selbst. -# -# Speicher nur am WR1 erlaubt! Bei zus. Speicher an WR2 stimmen die Werte nicht mehr! -# -# 2019 Kevin Wieland, Michael Ortenstein -# This file is part of openWB -# -# 01.09.2021 skl Überarbeitung Klassen basiert -######################################################### -import logging -from typing import List -import sys - -# only in python >3 available -# from packaging import version -# from timezone import timezone -from ipparser import ipparser -from pymodbus.payload import BinaryPayloadDecoder -from pymodbus.constants import Endian -from pymodbus.client.sync import ModbusTcpClient - -from helpermodules.cli import run_using_positional_cli_args - -log = logging.getLogger("Kostal-Plenticore") - -remotedebug = 0 -# zukünftige Nutzung ? -# try: -# import debugpy -# remotedebug=1 -# except ImportError, e: -# remotedebug=0 # module doesn't exist, deal with it. - - -class modbus: - def __init__(self, IP): - # Class Variablen - self._IP = IP - self._client = '' - # Plenticore als Modbus Client einrichten - self._client = ModbusTcpClient(self._IP, port=1502) - # self._wr.client.connect() - - def ReadRegister(self, address, count=1, **kwargs): - return self._client.read_holding_registers(address, count, **kwargs) - - def ReadUInt16(self, addr): - data = self._client.read_holding_registers(addr, 1, unit=71) - UInt16register = BinaryPayloadDecoder.fromRegisters( - data.registers, byteorder=Endian.Big, wordorder=Endian.Little) - result = UInt16register.decode_16bit_uint() - return(result) - - def ReadInt16(self, addr): - data = self._client.read_holding_registers(addr, 1, unit=71) - Int16register = BinaryPayloadDecoder.fromRegisters( - data.registers, byteorder=Endian.Big, wordorder=Endian.Little) - result = Int16register.decode_16bit_int() - return(result) - - def ReadUInt32(self, addr): - data = self._client.read_holding_registers(addr, 2, unit=71) - UInt32register = BinaryPayloadDecoder.fromRegisters( - data.registers, byteorder=Endian.Big, wordorder=Endian.Little) - result = UInt32register.decode_32bit_uint() - return(result) - - def ReadInt32(self, addr): - data = self._client.read_holding_registers(addr, 2, unit=71) - Int32register = BinaryPayloadDecoder.fromRegisters( - data.registers, byteorder=Endian.Big, wordorder=Endian.Little) - result = Int32register.decode_32bit_int() - return(result) - - def ReadFloat32(self, addr): - data = self._client.read_holding_registers(addr, 2, unit=71) - Float32register = BinaryPayloadDecoder.fromRegisters( - data.registers, byteorder=Endian.Big, wordorder=Endian.Little) - result = Float32register.decode_32bit_float() - return(result) - - def ReadUInt64(self, addr): - data = self._client.read_holding_registers(addr, 4, unit=71) - UInt64register = BinaryPayloadDecoder.fromRegisters( - data.registers, byteorder=Endian.Big, wordorder=Endian.Little) - result = UInt64register.decode_64bit_uint() - return(result) - - def ReadString(self, addr): - data = self._client.read_holding_registers(addr, 8, unit=71) - Stringregister = BinaryPayloadDecoder.fromRegisters( - data.registers, byteorder=Endian.Big, wordorder=Endian.Little) - result = Stringregister.decode_string(8) - return(result) - - -class plenticore_KSEM: - def __init__(self): - # Phase 1 - self.I_P1_A = 0 - # Phase 2 - self.I_P2_A = 0 - # Phase 3 - self.I_P3_A = 0 - # EVU Gesamt - self.P_active_total = 0 - # Frequenz - self.Freq = 0 - # Leistung P1-3 - self.P_P1_W = 0 - self.P_P2_W = 0 - self.P_P3_W = 0 - # Spannung P1-3 - self.U_P1_V = 0 - self.U_P2_V = 0 - self.U_P3_V = 0 - # cosphi - self.cosphi_actual = 0 - - -class plenticore_inverter: - def __init__(self): - self.MC_Version = "" - self.String_Count = 0 - self.P_DC_total = 0 - self.P_DC_S1 = 0 - self.P_DC_S2 = 0 - self.P_DC_S3 = 0 - self.P_DC_in_total = 0 - self.P_Generation_actual = 0 - self.P_PV_AC_total = 0 - self.P_Home_Cons_PV = 0 - self.P_Home_Cons_Grid = 0 - self.P_Home_Cons_Bat = 0 - self.Total_yield = 0 - self.Yearly_yield = 0 - self.Monthly_yield = 0 - - -class plenticore_battery: - def __init__(self): - self.Model = 0 - self.SerialNo = 0 - self.SoC_actual = 0 - self.P_charge_discharge = 0 - self.Capacity = 0 - - -class plenticore(modbus): - # Klassen Variablen - BatMgt = 0 - - def __init__(self, WRIP, Battery): - # Class Variablen - self._IP = WRIP - self._Battery = Battery - # KSEM - self.attr_KSEM = plenticore_KSEM() - # Wechselrichter - self.attr_WR = plenticore_inverter() - # Battery - self.attr_Bat = plenticore_battery() - # Plenticore als Modbus Client einrichten - try: - modbus.__init__(self, WRIP) - except: - # kein Zugriff auf WR1, also Abbruch und mit Null initialisierte Variablen in die Ramdisk - log.debug('Wechselrichter IP :' + self._IP) - log.debug('Fehler beim Initialisieren des Modbus-Client WR1: ' + - str(sys.exc_info()[0])) - - def ReadBattery(self): - # dann zunächst alle relevanten Register aus WR 1 auslesen: - try: - if self._Battery == 1: - # Plenticore Register 514: Battery_actual_SOC [%] - # ist Ladestand des Speichers - self.attr_Bat.SoC_actual = int(self.ReadInt16(514)) - # Plenticore Register 525: Battery_Model ID - self.attr_Bat.Model = int(self.ReadUInt32(525)) - # Plenticore Register 527: Battery_Serial - self.attr_Bat.SerialNo = int(self.ReadUInt32(527)) - # Plenticore Register 529: Battery_Capacity - self.attr_Bat.Capacity = int(self.ReadUInt32(529)) - # Plenticore Register 582: Actual_batt_ch_disch_power [W] - # ist Lade-/Entladeleistung des angeschlossenen Speichers - # {charge=negativ, discharge=positiv} - self.attr_Bat.P_charge_discharge = int(self.ReadInt16(582)) - except: - # kein Zugriff auf WR1, also Abbruch und mit 0 initialisierte Variablen in die Ramdisk - log.debug('Fehler beim Lesen der Modbus-Register Battery:' + - str(self._IP) + '(falsche IP?)' + str(sys.exc_info()[0])) - - def ReadWechselrichter(self): - try: - # Kostal Anzahl an PV Strings - self.attr_WR.String_Count = int(self.ReadUInt16(34)) - # FW Version of Kostal - self.attr_WR.MC_Version = str(self.ReadString(38)) - # Plenticore Register 260: Power DC1 [W] - # ist Leistung String 1 - self.attr_WR.P_DC_total = int(self.ReadFloat32(100)) - # Plenticore Register 106: Power Home Consumption Battery [W] - self.attr_WR.P_Home_Cons_Bat = int(self.ReadFloat32(106)) - # Plenticore Register 106: Power Home Consumption Grid [W] - self.attr_WR.P_Home_Cons_Grid = int(self.ReadFloat32(108)) - # Plenticore Register 116: Power Home Consumption PV [W] - self.attr_WR.P_Home_Cons_PV = int(self.ReadFloat32(116)) - # Plenticore Register 172: Total Active Power AC [W] - self.attr_WR.P_AC_Total = int(self.ReadFloat32(172)) - # Plenticore Register 260: Power DC1 [W] - # ist Leistung String 1 - self.attr_WR.P_DC_S1 = int(self.ReadFloat32(260)) - # Plenticore Register 270: Power DC2 [W] - # ist Leistung String 2 - self.attr_WR.P_DC_S2 = int(self.ReadFloat32(270)) - # Plenticore Register 270: Power DC3 [W] - # ist Leistung String 3 - self.attr_WR.P_DC_S3 = int(self.ReadFloat32(280)) - # Plenticore Register 320: Total_yield [Wh] - # ist PV Gesamtertrag - self.attr_WR.Total_yield = int(self.ReadFloat32(320)) - # Plenticore Register 324: Yearly_yield [Wh] - # ist PV Jahresertrag - self.attr_WR.Yearly_yield = round((self.ReadFloat32(324)/1000), 2) - # Plenticore Register 326: Monthly_yield [Wh] - # ist PV Monatsertrag - self.attr_WR.Monthly_yield = round((self.ReadFloat32(326)/1000), 2) - # Plenticore Register 575: Inverter_generation_power_actual [W] - # ist AC-Leistungsabgabe des Wechselrichters - self.attr_WR.P_Generation_actual = int(self.ReadInt16(575)) - # nur generierte PV Leistung berechnen, keine BatterieLeistung - self.attr_WR.P_DC_in_total = self.attr_WR.P_DC_S1 + self.attr_WR.P_DC_S2 - # im Fall ohne Batterie - if (self._Battery == 0): - # ggf. 3 String zu addieren - self.attr_WR.P_DC_in_total += self.attr_WR.P_DC_S3 - self.attr_WR.P_PV_AC_total = self.attr_WR.P_Generation_actual - # zur weiter Berechnung im Fall mit Batterie - else: - # da Batterie DC-seitig angebunden ist, - # muss deren Lade-/Entladeleistung mitbetrachtet werden - # wenn man die AC-Leistung der PV-Module und des Speichers bestimmen möchte. - # Kostal liefert nur DC-Werte, also DC-Leistung berechnen - # schauen, ob überhaupt PV-Leistung erzeugt wird - if self.attr_WR.P_DC_in_total < 0: - # PV-Anlage kann nichts verbrauchen, also ggf. Register-/Rundungsfehler korrigieren - self.attr_WR.P_PV_AC_total = 0 - else: - # wird PV-DC-Leistung erzeugt, müssen die Wandlungsverluste betrachtet werden - # Kostal liefert nur DC-seitige Werte - # zunächst Annahme, die Batterie wird geladen: - # die PV-Leistung die Summe aus verlustbehafteter AC-Leistungsabgabe des WR - # und der DC-Ladeleistung, die Wandlungsverluste werden also nur in der PV-Leistung - # ersichtlich - if self.attr_Bat.P_charge_discharge > 0: - # wird die Batterie entladen, werden die Wandlungsverluste anteilig an der - # DC-Leistung auf PV und Batterie verteilt - # dazu muss der Divisor Total_DC_power != 0 sein - Total_DC_power1 = self.attr_WR.P_DC_in_total + self.attr_Bat.P_charge_discharge - self.attr_WR.P_PV_AC_total = int( - (self.attr_WR.P_DC_in_total / float(Total_DC_power1)) * self.attr_WR.P_Generation_actual) - self.attr_Bat.P_charge_discharge = self.attr_WR.P_Generation_actual - self.attr_WR.P_PV_AC_total - else: - # Batterie wird geladen - # dann ist PV-Leistung die Wechselrichter-AC-Leistung + die Ladeleistung der Batterie - # (negative because charging) - self.attr_WR.P_PV_AC_total = self.attr_WR.P_Generation_actual - self.attr_Bat.P_charge_discharge - self.attr_WR.P_DC_in_total += self.attr_WR.P_DC_S3 - except: - # kein Zugriff auf WR1, also Abbruch und mit 0 initialisierte Variablen in die Ramdisk - log.debug("Fehler beim Lesen der Modbus-Register WR:" + str(self._IP) + - "(falsche WR-IP?)" + str(sys.exc_info()[0])) - - # Version Check to enable ModBus BatterMgt - not available before this version - # print("FW Version BatMgtSupport min=" + str(("1.47"))) - # print("FW Version Ist=" + str(self.attr_WR.MC_Version)) - # if version.parse(str(self.attr_WR.MC_Version)) >= version.parse("1.47"): - # self.BatMgt =1 - # else: - self.BatMgt = 0 - - def ReadKSEM300(self): - try: - # Strom auf Phasen 1-3 EVU aus Kostal Plenticore lesen - # Wechselrichter bekommt Daten von Energy Manager EM300 - # Phase 1 - # Plenticore Register 222: Current_phase_1_(powermeter) [A] - self.attr_KSEM.I_P1_A = round(self.ReadFloat32(222), 2) - # Phase 2 - # Plenticore Register 232: Current_phase_2_(powermeter) [A] - self.attr_KSEM.I_P2_A = round(self.ReadFloat32(232), 2) - # Phase 3 - # Plenticore Register 242: Current_phase_3_(powermeter) [A] - self.attr_KSEM.I_P3_A = round(self.ReadFloat32(242), 2) - # Leistung EVU - # Plenticore Register 252: Total_active_power_(powermeter) [W] - # Sensorposition 1 (Hausanschluss): (+)Hausverbrauch (-)Erzeugung - # Sensorposition 2 (EVU Anschlusspunkt): (+)Bezug (-)Einspeisung - self.attr_KSEM.P_active_total = int(self.ReadFloat32(252)) - # Frequenz EVU - # Plenticore Register 220: Frequency_(powermeter) [Hz] - self.attr_KSEM.Freq = round(self.ReadFloat32(220), 2) - # Leistung auf Phasen 1-3 EVU aus Kostal Plenticore lesen - # Wechselrichter bekommt Daten von Energy Manager EM300 - # Phase 1 - # Plenticore Register 224: Active_power_phase_1_(powermeter) [W] - self.attr_KSEM.P_P1_W = round(self.ReadFloat32(224), 2) - # Phase 2 - # Plenticore Register 234: Active_power_phase_2_(powermeter) [W] - self.attr_KSEM.P_P2_W = round(self.ReadFloat32(234), 2) - # Phase 3 - # Plenticore Register 244: Active_power_phase_3_(powermeter) [A] - self.attr_KSEM.P_P3_W = round(self.ReadFloat32(244), 2) - # Spannung auf Phasen 1-3 EVU aus Kostal Plenticore lesen - # Wechselrichter bekommt Daten von Energy Manager EM300 - # Phase 1 - # Plenticore Register 230: Voltage_phase_1_(powermeter) [V] - self.attr_KSEM.U_P1_V = round(self.ReadFloat32(230), 2) - # Phase 2 - # Plenticore Register 240: Voltage_phase_2_(powermeter) [V] - self.attr_KSEM.U_P2_V = round(self.ReadFloat32(240), 2) - # Phase 3 - # Plenticore Register 250: Voltage_phase_3_(powermeter) [V] - self.attr_KSEM.U_P3_V = round(self.ReadFloat32(250), 2) - # Plenticore Register 150: Actual_cos_phi [] - # ist Wirkfaktor - self.attr_KSEM.cosphi_actual = round(self.ReadFloat32(150), 3) - except: - # kein Zugriff auf WR1, also Abbruch und mit 0 initialisierte Variablen in die Ramdisk - log.debug("Fehler beim Lesen der Modbus-Register KSEM300:" + str(self.IP) - + "(falsche WR-IP?)" + str(sys.exc_info()[0])) - - def BatteryMgt(self): - # Battery Mgt. relevant Register aus WR 1 auslesen: - try: - if self._Battery == 1: - # Speicher am Planticore 1, erweiterte Control Register - # Plenticore Register 1068: Battery Work capacity [Wh] - # Kapazität Batterie - self.Bat_Capacity = round(self.ReadFloat32(1068), 2) - # Plenticore Register 1070: Battery SerialNumber - # Seriennummer der Batterie - self.Bat_Serial = str(self.ReadString(1070)) - # Battery mgt. mode - self.Bat_ControlMode = int(self.ReadInt32(1080)) - # print("mode = " + self.Bat_ControlMode) - # ist Kostal WR auf Battery Mgt. per Modbus Cfg - # 0 = intern, 1 = digital IO's, 2= ModBus Extern - if self.Bat_ControlMode == 2: - # Speicher am Planticore 1, erweiterte Control Register - # Plenticore Register 1026 Start Address for Battery Mgt. - # Battery charge power (AC) setpoint, absolute [W] - self.Bat_AbsSet_AC_ChargePower = round(self.ReadFloat32(1026), 2) - # Battery charge current (DC) setpoint, relative [%] - self.Bat_RelSet_DC_ChargeCurrent = round(self.ReadFloat32(1028), 2) - # Battery charge power (AC) setpoint, relative [%] - self.Bat_RelSet_AC_ChargePower = round(self.ReadFloat32(1030), 2) - # Battery charge current (DC) setpoint, absolute [A] - self.Bat_AbsSet_DC_ChargeCurrent = round(self.ReadFloat32(1032), 2) - except: - # kein Zugriff auf WR1, also Abbruch und mit 0 initialisierte Variablen in die Ramdisk - log.debug("Fehler beim Lesen der Modbus-Register BatteryMgt.:" + - str(self._IP) + "(falsche IP?)" + str(sys.exc_info()[0])) - - -def update(WR1IP: str, WR2IP: str, Battery: int, WR3IP: str): - WR4IP = "none" - WR5IP = "none" - ips = ipparser(WR3IP) - # in IP3 kann ein aufeinanderfolgende Liste enthalten sein "192.168.0.1-3" - if len(ips) > 1: - WR3IP = ips[0] - WR4IP = ips[1] - WR5IP = ips[2] - - # Variablen initialisieren - # Summenwerte - PV_power_total = 0 - Total_yield = 0 - Yearly_yield = 0 - Monthly_yield = 0 - WR1 = None - WR2 = None - WR3 = None - WR4 = None - WR5 = None - - WR1 = plenticore(WR1IP, Battery) - log.debug("Wechselrichter Kostal Plenticore Config - WR1:" + str(WR1IP) + " -WR2:" + str(WR2IP) + - "\n -Battery:" + str(Battery) + " -WR3:" + str(WR3IP) + "-WR4:" + str(WR4IP) + "-WR5:" + str(WR5IP)) - - WR1.ReadKSEM300() - # Battery nur an WR1 erlaubt - if Battery == 1: - WR1.ReadBattery() - - WR1.ReadWechselrichter() - - # zukünftige Implementierung von Battery Steuerung - # Battery nur an WR1 erlaubt - if WR1.BatMgt == 1: - WR1.BatteryMgt() - - # am WR2 darf keine Batterie sein, deswegen hier vereinfacht PV-Leistung = AC-Leistung des WR - if WR2IP != "none": - WR2 = plenticore(WR2IP, 0) - - if WR3IP != "none": - WR3 = plenticore(WR3IP, 0) - - # am WR2 darf keine Batterie sein, deswegen hier vereinfacht PV-Leistung = AC-Leistung des WR - if WR4IP != "none": - WR4 = plenticore(WR4IP, 0) - - if WR5IP != "none": - WR5 = plenticore(WR5IP, 0) - - # Summen der Erträge bestimmen - PV_power_total = WR1.attr_WR.P_PV_AC_total - log.debug("WR1 Leistung = " + str(WR1.attr_WR.P_PV_AC_total) + "PV_total = " + str(PV_power_total)) - Total_yield = WR1.attr_WR.Total_yield - Monthly_yield = WR1.attr_WR.Monthly_yield - Yearly_yield = WR1.attr_WR.Yearly_yield - - # ggf. dekodierte Register WR 2 in entsprechende Typen umwandeln - if WR2 is not None: - WR2.ReadWechselrichter() - PV_power_total += WR2.attr_WR.P_PV_AC_total - log.debug("WR2 Leistung = " + str(WR2.attr_WR.P_PV_AC_total) + - "PV_total = " + str(PV_power_total)) - # Summen der Erträge bestimmen - Total_yield += WR2.attr_WR.Total_yield - Monthly_yield += WR2.attr_WR.Monthly_yield - Yearly_yield += WR2.attr_WR.Yearly_yield - - # ggf. dekodierte Register WR 3 in entsprechende Typen umwandeln - if WR3 is not None: - WR3.ReadWechselrichter() - PV_power_total += WR3.attr_WR.P_PV_AC_total - log.debug("WR3 Leistung = " + str(WR3.attr_WR.P_PV_AC_total) + - "PV_total = " + str(PV_power_total)) - # Summen der Erträge bestimmen - Total_yield += WR3.attr_WR.Total_yield - Monthly_yield += WR3.attr_WR.Monthly_yield - Yearly_yield += WR3.attr_WR.Yearly_yield - - # ggf. dekodierte Register WR 4 in entsprechende Typen umwandeln - if WR4 is not None: - WR4.ReadWechselrichter() - PV_power_total += WR4.attr_WR.P_PV_AC_total - log.debug("WR4 Leistung = " + str(WR4.attr_WR.P_PV_AC_total) + - "PV_total = " + str(PV_power_total)) - # Summen der Erträge bestimmen - Total_yield += WR4.attr_WR.Total_yield - Monthly_yield += WR4.attr_WR.Monthly_yield - Yearly_yield += WR4.attr_WR.Yearly_yield - - # ggf. dekodierte Register WR 5 in entsprechende Typen umwandeln - if WR5 is not None: - WR5.ReadWechselrichter() - PV_power_total += WR5.attr_WR.P_PV_AC_total - log.debug("WR5 Leistung = " + str(WR5.attr_WR.P_PV_AC_total) + - "PV_total = " + str(PV_power_total)) - # Summen der Erträge bestimmen - Total_yield += WR5.attr_WR.Total_yield - Monthly_yield += WR5.attr_WR.Monthly_yield - Yearly_yield += WR5.attr_WR.Yearly_yield - - # Batteriewerte Berechnen und übertragen - if Battery == 1: - # Speicherladung muss durch Wandlungsverluste und internen Verbrauch korrigiert werden - # sonst wird ein falscher Hausverbrauch berechnet - # die Verluste fallen hier unter den Tische, besser wäre auch HomeConsumption direkt zu openWB - if WR1.attr_Bat.P_charge_discharge >= 0: - P_Charge_corrected = WR1.attr_Bat.P_charge_discharge - \ - (WR1.attr_Bat.P_charge_discharge - WR1.attr_WR.P_Home_Cons_Bat) - # print("Leistung Speicher Modbus= " + str(WR1.attr_Bat.P_charge_discharge) + - # " ,Speicherladung korrigiert= " + str(P_Charge_corrected) + - # ", Leistung Hausverbrauch Bat actual = " + str(WR1.attr_WR.P_Home_Cons_Bat)) - else: - P_Charge_corrected = WR1.attr_Bat.P_charge_discharge - - # Nachfolgende Werte nur in temporäre ramdisk, da die Module - # Speicher und Bezug für die globalen Variablen zuständig sind - # und dort die Übernahme in die entsprechende ramdisk erfolgt - # Speicherleistung WR 1 - with open('/var/www/html/openWB/ramdisk/temp_speicherleistung', 'w') as f: - f.write(str(P_Charge_corrected*-1)) - # Speicher Ladestand von Speicher am WR 1 - with open('/var/www/html/openWB/ramdisk/temp_speichersoc', 'w') as f: - f.write(str(WR1.attr_Bat.SoC_actual)) - - # zunächst alle Summenwerte beider WR - # Gesamtleistung AC PV-Module - with open('/var/www/html/openWB/ramdisk/pvwatt', 'w') as f: - f.write(str(PV_power_total*-1)) - # print("PV Leistung alle WR =" + str(PV_power_total*-1)) - - # Gesamtertrag in Wattstunden - # schreibe den Wert nur wenn kein Speicher vorhanden ist. Wenn er da ist nutze die openWB PV Watt Beschränkung - if Battery != 1: - with open('/var/www/html/openWB/ramdisk/pvkwh', 'w') as f: - f.write(str(Total_yield)) - - # Gesamtertrag in Kilowattstunden - with open('/var/www/html/openWB/ramdisk/pvkwhk', 'w') as f: - f.write(str(Total_yield / 1000)) - # Jahresertrag in Kilowattstunden - with open('/var/www/html/openWB/ramdisk/yearly_pvkwhk', 'w') as f: - f.write(str(Yearly_yield)) - # Monatsertrag in Kilowattstunden - with open('/var/www/html/openWB/ramdisk/monthly_pvkwhk', 'w') as f: - f.write(str(Monthly_yield)) - - # Werte WR 1 - # Leistung DC PV-Module - # die Variablen dürfen nicht der Nomenklatur openWB entsprechen - # Bsp. pv1watt oder pv2watt, da diese für PV Module 1 und 2 reserviert sind - # und nicht aus einem Module geschrieben werden dürfen - with open('/var/www/html/openWB/ramdisk/pvwatt1', 'w') as f: - f.write(str(WR1.attr_WR.P_PV_AC_total*-1)) - # Gesamtertrag in Wattstunden - with open('/var/www/html/openWB/ramdisk/pvkwh1', 'w') as f: - f.write(str(WR1.attr_WR.Total_yield)) - # Gesamtertrag in Kilowattstunden - with open('/var/www/html/openWB/ramdisk/pvkwhk1', 'w') as f: - f.write(str(WR1.attr_WR.Total_yield / 1000)) - # Jahresertrag in Kilowattstunden - with open('/var/www/html/openWB/ramdisk/yearly_pvkwhk1', 'w') as f: - f.write(str(WR1.attr_WR.Yearly_yield)) - # Monatsertrag in Kilowattstunden - with open('/var/www/html/openWB/ramdisk/monthly_pvkwhk1', 'w') as f: - f.write(str(WR1.attr_WR.Monthly_yield)) - - if WR2 is not None: - # Werte WR 2 - # Leistung DC PV-Module - with open('/var/www/html/openWB/ramdisk/pvwatt2', 'w') as f: - f.write(str(WR2.attr_WR.P_PV_AC_total*-1)) - # Gesamtertrag in Wattstunden - with open('/var/www/html/openWB/ramdisk/pvkwh2', 'w') as f: - f.write(str(WR2.attr_WR.Total_yield)) - # Gesamtertrag in Kilowattstunden - with open('/var/www/html/openWB/ramdisk/pvkwhk2', 'w') as f: - f.write(str(WR2.attr_WR.Total_yield / 1000)) - # Jahresertrag in Kilowattstunden - with open('/var/www/html/openWB/ramdisk/yearly_pvkwhk2', 'w') as f: - f.write(str(WR2.attr_WR.Yearly_yield)) - # Monatsertrag in Kilowattstunden - with open('/var/www/html/openWB/ramdisk/monthly_pvkwhk2', 'w') as f: - f.write(str(WR2.attr_WR.Monthly_yield)) - - if WR3 is not None: - # Werte WR 3 - # Leistung DC PV-Module - with open('/var/www/html/openWB/ramdisk/pvwatt3', 'w') as f: - f.write(str(WR3.attr_WR.P_PV_AC_total*-1)) - # Gesamtertrag in Wattstunden - with open('/var/www/html/openWB/ramdisk/pvkwh3', 'w') as f: - f.write(str(WR3.attr_WR.Total_yield)) - # Gesamtertrag in Kilowattstunden - with open('/var/www/html/openWB/ramdisk/pvkwhk3', 'w') as f: - f.write(str(WR3.attr_WR.Total_yield / 1000)) - # Jahresertrag in Kilowattstunden - with open('/var/www/html/openWB/ramdisk/yearly_pvkwhk3', 'w') as f: - f.write(str(WR3.attr_WR.Yearly_yield)) - # Monatsertrag in Kilowattstunden - with open('/var/www/html/openWB/ramdisk/monthly_pvkwhk3', 'w') as f: - f.write(str(WR3.attr_WR.Monthly_yield)) - - # Bezug EVU - with open('/var/www/html/openWB/ramdisk/temp_wattbezug', 'w') as f: - f.write(str(WR1.attr_KSEM.P_active_total)) - # print("KSEM Watt Bezug = " + str(WR1.attr_KSEM.P_active_total)) - # Bezug Strom Phase 1 - with open('/var/www/html/openWB/ramdisk/temp_bezuga1', 'w') as f: - f.write(str(WR1.attr_KSEM.I_P1_A)) - # Bezug Strom Phase 2 - with open('/var/www/html/openWB/ramdisk/temp_bezuga2', 'w') as f: - f.write(str(WR1.attr_KSEM.I_P2_A)) - # Bezug Strom Phase 3 - with open('/var/www/html/openWB/ramdisk/temp_bezuga3', 'w') as f: - f.write(str(WR1.attr_KSEM.I_P3_A)) - # Netzfrequenz - with open('/var/www/html/openWB/ramdisk/temp_evuhz', 'w') as f: - f.write(str(WR1.attr_KSEM.Freq)) - # Bezug Leistung Phase 1 - with open('/var/www/html/openWB/ramdisk/temp_bezugw1', 'w') as f: - f.write(str(WR1.attr_KSEM.P_P1_W)) - # Bezug Leistung Phase 2 - with open('/var/www/html/openWB/ramdisk/temp_bezugw2', 'w') as f: - f.write(str(WR1.attr_KSEM.P_P2_W)) - # Bezug Leistung Phase 3 - with open('/var/www/html/openWB/ramdisk/temp_bezugw3', 'w') as f: - f.write(str(WR1.attr_KSEM.P_P3_W)) - # Spannung Phase 1 - with open('/var/www/html/openWB/ramdisk/temp_evuv1', 'w') as f: - f.write(str(WR1.attr_KSEM.U_P1_V)) - # Spannung Phase 2 - with open('/var/www/html/openWB/ramdisk/temp_evuv2', 'w') as f: - f.write(str(WR1.attr_KSEM.U_P2_V)) - # Spannung Phase 3 - with open('/var/www/html/openWB/ramdisk/temp_evuv3', 'w') as f: - f.write(str(WR1.attr_KSEM.U_P3_V)) - # Wirkfaktor, wird nur einmal vom Wechselrichter ausgegeben, - # und ist demnach für alle Phasen identisch - with open('/var/www/html/openWB/ramdisk/temp_evupf1', 'w') as f: - f.write(str(WR1.attr_KSEM.cosphi_actual)) - with open('/var/www/html/openWB/ramdisk/temp_evupf2', 'w') as f: - f.write(str(WR1.attr_KSEM.cosphi_actual)) - with open('/var/www/html/openWB/ramdisk/temp_evupf3', 'w') as f: - f.write(str(WR1.attr_KSEM.cosphi_actual)) - - -def main(argv: List[str]): - run_using_positional_cli_args(update, argv) diff --git a/packages/modules/kostal_plenticore/__init__.py b/packages/modules/kostal_plenticore/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/modules/kostal_plenticore/bat.py b/packages/modules/kostal_plenticore/bat.py new file mode 100644 index 000000000..79010032e --- /dev/null +++ b/packages/modules/kostal_plenticore/bat.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +from typing import Any, Callable +from modules.common.component_state import BatState +from modules.common.component_type import ComponentDescriptor +from modules.common.modbus import ModbusDataType +from modules.common.fault_state import ComponentInfo +from modules.common.simcount import SimCounter +from modules.common.store import get_bat_value_store +from modules.kostal_plenticore.config import KostalPlenticoreBatSetup + + +class KostalPlenticoreBat: + def __init__(self, + device_id: int, + component_config: KostalPlenticoreBatSetup) -> None: + self.component_config = component_config + self.store = get_bat_value_store(self.component_config.id) + self.sim_counter = SimCounter(device_id, self.component_config.id, prefix="speicher") + self.component_info = ComponentInfo.from_component_config(self.component_config) + + def read_state(self, reader: Callable[[int, ModbusDataType], Any]) -> BatState: + power = reader(582, ModbusDataType.INT_16) * -1 + soc = reader(514, ModbusDataType.INT_16) + imported, exported = self.sim_counter.sim_count(power) + + return BatState( + power=power, + soc=soc, + imported=imported, + exported=exported, + ) + + def update(self, state): + self.store.set(state) + + +component_descriptor = ComponentDescriptor(configuration_factory=KostalPlenticoreBatSetup) diff --git a/packages/modules/kostal_plenticore/config.py b/packages/modules/kostal_plenticore/config.py new file mode 100644 index 000000000..71b962ed5 --- /dev/null +++ b/packages/modules/kostal_plenticore/config.py @@ -0,0 +1,62 @@ +from typing import Optional + +from modules.common.component_setup import ComponentSetup + + +class KostalPlenticoreConfiguration: + def __init__(self, ip_address: Optional[str] = None): + self.ip_address = ip_address + + +class KostalPlenticore: + def __init__(self, + name: str = "Kostal Plenticore", + type: str = "kostal_plenticore", + id: int = 0, + configuration: KostalPlenticoreConfiguration = None) -> None: + self.name = name + self.type = type + self.id = id + self.configuration = configuration or KostalPlenticoreConfiguration() + + +class KostalPlenticoreBatConfiguration: + def __init__(self): + pass + + +class KostalPlenticoreBatSetup(ComponentSetup[KostalPlenticoreBatConfiguration]): + def __init__(self, + name: str = "Kostal Plenticore Speicher", + type: str = "bat", + id: int = 0, + configuration: KostalPlenticoreBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration or KostalPlenticoreBatConfiguration()) + + +class KostalPlenticoreCounterConfiguration: + def __init__(self): + pass + + +class KostalPlenticoreCounterSetup(ComponentSetup[KostalPlenticoreCounterConfiguration]): + def __init__(self, + name: str = "Kostal Plenticore Zähler", + type: str = "counter", + id: Optional[int] = 0, + configuration: KostalPlenticoreCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or KostalPlenticoreCounterConfiguration()) + + +class KostalPlenticoreInverterConfiguration: + def __init__(self): + pass + + +class KostalPlenticoreInverterSetup(ComponentSetup[KostalPlenticoreInverterConfiguration]): + def __init__(self, + name: str = "Kostal Plenticore Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: KostalPlenticoreInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or KostalPlenticoreInverterConfiguration()) diff --git a/packages/modules/kostal_plenticore/counter.py b/packages/modules/kostal_plenticore/counter.py new file mode 100644 index 000000000..a8135c78a --- /dev/null +++ b/packages/modules/kostal_plenticore/counter.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +from typing import Any, Callable +from modules.common.component_state import CounterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo +from modules.common.modbus import ModbusDataType +from modules.common.simcount import SimCounter +from modules.common.store import get_counter_value_store +from modules.kostal_plenticore.config import KostalPlenticoreCounterSetup + + +class KostalPlenticoreCounter: + def __init__(self, + device_id: int, + component_config: KostalPlenticoreCounterSetup) -> None: + self.component_config = component_config + self.store = get_counter_value_store(self.component_config.id) + self.sim_counter = SimCounter(device_id, self.component_config.id, prefix="bezug") + self.component_info = ComponentInfo.from_component_config(self.component_config) + + def get_values(self, reader: Callable[[int, ModbusDataType], Any]) -> CounterState: + power_factor = reader(150, ModbusDataType.FLOAT_32) + currents = [reader(register, ModbusDataType.FLOAT_32) for register in [222, 232, 242]] + voltages = [reader(register, ModbusDataType.FLOAT_32) for register in [230, 240, 250]] + powers = [reader(register, ModbusDataType.FLOAT_32) for register in [224, 234, 244]] + power = reader(252, ModbusDataType.FLOAT_32) + frequency = reader(220, ModbusDataType.FLOAT_32) + imported, exported = self.sim_counter.sim_count(power) + + return CounterState( + powers=powers, + currents=currents, + voltages=voltages, + imported=imported, + exported=exported, + power=power, + power_factors=[power_factor]*3, + frequency=frequency + ) + + def update_imported_exported(self, state): + state.imported, state.exported = self.sim_counter.sim_count(state.power) + return state + + def update(self, reader: Callable[[int, ModbusDataType], Any]): + self.store.set(self.update_imported_exported(self.get_values(reader))) + + +component_descriptor = ComponentDescriptor(configuration_factory=KostalPlenticoreCounterSetup) diff --git a/packages/modules/kostal_plenticore/device.py b/packages/modules/kostal_plenticore/device.py new file mode 100644 index 000000000..6d9214a48 --- /dev/null +++ b/packages/modules/kostal_plenticore/device.py @@ -0,0 +1,166 @@ +# !/usr/bin/env python3 +from enum import IntEnum +from ipparser import ipparser +from itertools import chain +from typing import Any, Callable, Iterable, List, Union +from pymodbus.constants import Endian +import functools +import logging + +from helpermodules.cli import run_using_positional_cli_args +from modules.common import modbus +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_state import BatState, InverterState +from modules.common.configurable_device import ConfigurableDevice, ComponentFactoryByType, MultiComponentUpdater +from modules.common.store import get_counter_value_store +from modules.kostal_plenticore.bat import KostalPlenticoreBat +from modules.kostal_plenticore.inverter import KostalPlenticoreInverter +from modules.kostal_plenticore.config import (KostalPlenticore, KostalPlenticoreBatSetup, KostalPlenticoreCounterSetup, + KostalPlenticoreInverterSetup) +from modules.kostal_plenticore.counter import KostalPlenticoreCounter + + +log = logging.getLogger(__name__) + + +class LegacyCounterPosition(IntEnum): + HOME_CONSUMPTION = 0 + GRID = 1 + + +def update( + components: Iterable[Union[KostalPlenticoreBat, KostalPlenticoreCounter, KostalPlenticoreInverter]], + reader: Callable[[int, modbus.ModbusDataType], Any], + set_inverter_state: bool = True): + battery = next((component for component in components if isinstance(component, KostalPlenticoreBat)), None) + bat_state_net = battery.read_state(reader) if battery else None + for component in components: + if isinstance(component, KostalPlenticoreInverter): + inverter_state = component.read_state(reader) + if bat_state_net: + dc_in = component.dc_in_string_1_2(reader) + home_consumption = component.home_consumption(reader) + if dc_in >= 0: + # Wird PV-DC-Leistung erzeugt, müssen die Wandlungsverluste betrachtet werden. + # Kostal liefert nur DC-seitige Werte. + if bat_state_net.power < 0: + # Wird die Batterie entladen, werden die Wandlungsverluste anteilig an der DC-Leistung auf PV + # und Batterie verteilt. Dazu muss der Divisor Total_DC_power != 0 sein. + power_gross = dc_in - bat_state_net.power + pv_state = InverterState(power=dc_in / power_gross * inverter_state.power, + exported=inverter_state.exported) + # Speicherladung muss durch Wandlungsverluste und internen Verbrauch korrigiert werden, sonst + # wird ein falscher Hausverbrauch berechnet. Die Verluste fallen hier unter den Tisch. + bat_state_gross = BatState(power=pv_state.power-inverter_state.power-home_consumption, + imported=bat_state_net.imported, + exported=bat_state_net.exported) + else: + # Wenn die Batterie geladen wird, dann ist PV-Leistung die Wechselrichter-AC-Leistung + die + # Ladeleistung der Batterie. Die PV-Leistung ist die Summe aus verlustbehafteter + # AC-Leistungsabgabe des WR und der DC-Ladeleistung. Die Wandlungsverluste werden also nur + # in der PV-Leistung ersichtlich. + pv_state = InverterState(power=inverter_state.power - bat_state_net.power, + exported=inverter_state.exported) + bat_state_gross = bat_state_net + # https://github.com/snaptec/openWB/pull/2440#discussion_r996275286 + # power_gross = bat_state.power + dc_in + # bat_state_gross = BatteryState(power=bat_state_net.power / power_gross * inverter_state.power) + # pv_state = InverterState(power=inverter_state.power - bat_state_gross.power) + else: + inverter_state.power = 0 + pv_state = inverter_state + bat_state_gross = bat_state_net + else: + pv_state = inverter_state + if set_inverter_state: + component.update(pv_state) + elif isinstance(component, KostalPlenticoreCounter): + component.update(reader) + if bat_state_net: + battery.update(bat_state_gross) + if set_inverter_state is False: + return pv_state + + +def create_device(device_config: KostalPlenticore): + def create_bat_component(component_config): + return KostalPlenticoreBat(device_config.id, component_config) + + def create_counter_component(component_config): + return KostalPlenticoreCounter(device_config.id, component_config) + + def create_inverter_component(component_config): + return KostalPlenticoreInverter(component_config) + + def update_components( + components: Iterable[Union[KostalPlenticoreBat, KostalPlenticoreCounter, KostalPlenticoreInverter]] + ): + with tcp_client: + update(components, reader) + + tcp_client = modbus.ModbusTcpClient_(device_config.configuration.ip_address, 1502) + reader = _create_reader(tcp_client) + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component, counter=create_counter_component, inverter=create_inverter_component), + component_updater=MultiComponentUpdater(update_components), + ) + + +def _create_reader(tcp_client: modbus.ModbusTcpClient_) -> Callable[[int, modbus.ModbusDataType], Any]: + return functools.partial(tcp_client.read_holding_registers, unit=71, wordorder=Endian.Little) + + +def read_legacy_inverter(ip1: str, ip2: str, battery: int, ip3: str) -> InverterState: + # in IP3 kann ein aufeinanderfolgende Liste enthalten sein "192.168.0.1-3" + log.debug("Kostal Plenticore: WR1: %s, WR2: %s, WR3: %s, Battery: %s", ip1, ip2, ip3, battery) + inverter_component = KostalPlenticoreInverter(KostalPlenticoreInverterSetup(id=1)) + + def get_hybrid_inverter_state(ip: str) -> InverterState: + battery_component = KostalPlenticoreBat(1, KostalPlenticoreBatSetup()) + with modbus.ModbusTcpClient_(ip, 1502) as client: + return update( + [inverter_component, battery_component], _create_reader(client), set_inverter_state=False + ) + + def get_standard_inverter_state(ip: str) -> InverterState: + with modbus.ModbusTcpClient_(ip, 1502) as client: + return inverter_component.read_state(_create_reader(client)) + + def inverter_state_sum(a: InverterState, b: InverterState) -> InverterState: + return InverterState(exported=a.exported + b.exported, power=a.power + b.power) + + inverter_state = functools.reduce( + inverter_state_sum, + map(get_standard_inverter_state, filter("none".__ne__, chain([ip2], ipparser(ip3)))), + get_hybrid_inverter_state(ip1) if battery else get_standard_inverter_state(ip1) + ) + inverter_component.update(inverter_state) + return inverter_state + + +def read_legacy_counter(ip1: str, ip2: str, battery: int, ip3: str, position: int) -> None: + log.debug("Kostal Plenticore: WR1: %s, Position: %s", ip1, position) + client = modbus.ModbusTcpClient_(ip1, 1502) + reader = _create_reader(client) + counter_component = KostalPlenticoreCounter(None, KostalPlenticoreCounterSetup(id=None)) + if LegacyCounterPosition(position) == LegacyCounterPosition.GRID: + with client: + counter_component.update(reader) + else: + with client: + counter_state = counter_component.get_values(reader) + bat_power = KostalPlenticoreBat(None, KostalPlenticoreBatSetup()).read_state(reader).power + inverter_power = read_legacy_inverter(ip1, ip2, battery, ip3).power + counter_state.power += inverter_power + bat_power + counter_state = counter_component.update_imported_exported(counter_state) + get_counter_value_store(None).set(counter_state) + + +def main(argv: List[str]): + run_using_positional_cli_args({"counter": read_legacy_counter, "inverter": read_legacy_inverter}, argv + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=KostalPlenticore) diff --git a/packages/modules/kostal_plenticore/inverter.py b/packages/modules/kostal_plenticore/inverter.py new file mode 100644 index 000000000..b275609c4 --- /dev/null +++ b/packages/modules/kostal_plenticore/inverter.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +from typing import Any, Callable +from modules.common.component_state import InverterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo +from modules.common.modbus import ModbusDataType +from modules.common.store import get_inverter_value_store +from modules.kostal_plenticore.config import KostalPlenticoreInverterSetup + + +class KostalPlenticoreInverter: + def __init__(self, + component_config: KostalPlenticoreInverterSetup) -> None: + self.component_config = component_config + self.store = get_inverter_value_store(self.component_config.id) + self.component_info = ComponentInfo.from_component_config(self.component_config) + + def read_state(self, reader: Callable[[int, ModbusDataType], Any]) -> InverterState: + # PV-Anlage kann nichts verbrauchen, also ggf. Register-/Rundungsfehler korrigieren. + power = reader(575, ModbusDataType.INT_16) * -1 + exported = reader(320, ModbusDataType.FLOAT_32) + + return InverterState( + power=power, + exported=exported + ) + + def dc_in_string_1_2(self, reader: Callable[[int, ModbusDataType], Any]): + return reader(260, ModbusDataType.FLOAT_32) + reader(270, ModbusDataType.FLOAT_32) + + def home_consumption(self, reader: Callable[[int, ModbusDataType], Any]): + return reader(106, ModbusDataType.FLOAT_32) + + def update(self, state): + self.store.set(state) + + +component_descriptor = ComponentDescriptor(configuration_factory=KostalPlenticoreInverterSetup)