From 2186c4d7dbcee13ce186aa11a4c411c5970e4372 Mon Sep 17 00:00:00 2001 From: Nils Reiners Date: Sun, 14 Sep 2025 10:52:50 +0200 Subject: [PATCH] wechselrichter zum tesent eingebunden --- SG_ready_schalten.py | 31 +++++ __pycache__/shelly_pro_3m.cpython-312.pyc | Bin 3721 -> 3719 bytes data/.~lock.shelly_pro_3m_registers.xlsx# | 1 - data_base_csv.py | 4 +- heat_pump.py | 4 +- main.py | 13 ++- .../heat_pump_registers.xlsx | Bin .../shelly_pro_3m_registers.xlsx | Bin shelly_pro_3m.py | 8 +- test_wr.py | 110 ++++++++++++++++++ 10 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 SG_ready_schalten.py delete mode 100644 data/.~lock.shelly_pro_3m_registers.xlsx# rename {data => modbus_registers}/heat_pump_registers.xlsx (100%) rename {data => modbus_registers}/shelly_pro_3m_registers.xlsx (100%) create mode 100644 test_wr.py diff --git a/SG_ready_schalten.py b/SG_ready_schalten.py new file mode 100644 index 0000000..e0b1988 --- /dev/null +++ b/SG_ready_schalten.py @@ -0,0 +1,31 @@ +from pymodbus.client import ModbusTcpClient + +def write_coils(ip): + # IP und Port der Wärmepumpe + port = 502 + client = ModbusTcpClient(ip, port=port) + + if not client.connect(): + print("Verbindung zur Wärmepumpe fehlgeschlagen.") + return + + try: + # Coil 300 = Kommunikation über Bus aktivieren (1) + response_300 = client.write_coil(300, True) + # Coil 301 = SG Ready Stufe 1 aktivieren (1) + response_301 = client.write_coil(301, False) + # Coil 302 = SG Ready Stufe 2 deaktivieren (0) + response_302 = client.write_coil(302, False) + + # Optional: Rückmeldungen prüfen + for addr, resp in zip([300, 301, 302], [response_300, response_301, response_302]): + if resp.isError(): + print(f"Fehler beim Schreiben von Coil {addr}: {resp}") + else: + print(f"Coil {addr} erfolgreich geschrieben.") + + finally: + client.close() + +# Testaufruf mit IP-Adresse deiner Wärmepumpe +write_coils("10.0.0.10") # <-- IP-Adresse hier anpassen diff --git a/__pycache__/shelly_pro_3m.cpython-312.pyc b/__pycache__/shelly_pro_3m.cpython-312.pyc index 4cae6d0942de6a94e418ed7256796551f0b0ce62..d2250f0bfb3c24c71a77f938cc415180c5d4334b 100644 GIT binary patch delta 118 zcmeB_ZI|Ue&CAQh00cgVO)^ewV&B>|M^~q09 uPc53P&mt;UrHNNbGRq4Qmn#CUcLYQx1WsYU!y|Y>$o2}4-R5{!ElvQ~8Yh|n delta 136 zcmZpd?Udy`&CAQh00hzpO)`#bJOU=khPc2T)$Vp63&C`3y2vlCGj#tm*M3xsKPFDn+?+A!Y2%N%xhez;&ko6TF Ko6WJTTATo_?Jdm! diff --git a/data/.~lock.shelly_pro_3m_registers.xlsx# b/data/.~lock.shelly_pro_3m_registers.xlsx# deleted file mode 100644 index fda7e61..0000000 --- a/data/.~lock.shelly_pro_3m_registers.xlsx# +++ /dev/null @@ -1 +0,0 @@ -,nils,nils-ThinkPad-P52,26.05.2025 20:45,file:///home/nils/.config/libreoffice/4; \ No newline at end of file diff --git a/data_base_csv.py b/data_base_csv.py index 656fcd7..beba130 100644 --- a/data_base_csv.py +++ b/data_base_csv.py @@ -18,7 +18,7 @@ class DataBaseCsv: writer.writerow(data) return - # If file exists → read existing header and data + # If file exists → read existing header and modbus_registers with open(self.filename, mode='r', newline='') as csv_file: reader = csv.DictReader(csv_file) existing_fields = reader.fieldnames @@ -39,7 +39,7 @@ class DataBaseCsv: for row in existing_data: writer.writerow({field: row.get(field, '') for field in all_fields}) - # Write new data row + # Write new modbus_registers row writer.writerow({field: data.get(field, '') for field in all_fields}) # Replace original file with updated temporary file diff --git a/heat_pump.py b/heat_pump.py index 92b7dc7..008dc4c 100644 --- a/heat_pump.py +++ b/heat_pump.py @@ -26,7 +26,7 @@ class HeatPump: def get_registers(self): # Excel-Datei mit den Input-Registerinformationen - excel_path = "data/heat_pump_registers.xlsx" + excel_path = "modbus_registers/heat_pump_registers.xlsx" xls = pd.ExcelFile(excel_path) df_input_registers = xls.parse('04 Input Register') @@ -43,7 +43,7 @@ class HeatPump: for _, row in df_clean.iterrows() } - def get_data(self): + def get_state(self): data = {} data['Zeit'] = time.strftime('%Y-%m-%d %H:%M:%S') for address, info in self.registers.items(): diff --git a/main.py b/main.py index 72989c0..3642708 100644 --- a/main.py +++ b/main.py @@ -5,7 +5,7 @@ from data_base_influx import DataBaseInflux from heat_pump import HeatPump from shelly_pro_3m import ShellyPro3m -interval = 10 +interval_seconds = 10 db = DataBaseInflux( url="http://localhost:8086", @@ -16,11 +16,16 @@ db = DataBaseInflux( hp = HeatPump(device_name='hp_master', ip_address='10.0.0.10') shelly = ShellyPro3m(device_name='wohnung_2_6', ip_address='192.168.1.121') +wr = SolarEdgeWechselrichter(device_name='wr_master', ip_address='192.168.1.112') + +controller = SgReadyController(hp, wr) while True: now = datetime.now() - if now.second % interval == 0 and now.microsecond < 100_000: - db.store_data(hp.device_name, hp.get_data()) - db.store_data(shelly.device_name, shelly.get_data()) + if now.second % interval_seconds == 0 and now.microsecond < 100_000: + db.store_data(hp.device_name, hp.get_state()) + db.store_data(shelly.device_name, shelly.get_state()) + db.store_data(wr.device_name, wr.get_state()) + controller.perform_action() time.sleep(0.1) diff --git a/data/heat_pump_registers.xlsx b/modbus_registers/heat_pump_registers.xlsx similarity index 100% rename from data/heat_pump_registers.xlsx rename to modbus_registers/heat_pump_registers.xlsx diff --git a/data/shelly_pro_3m_registers.xlsx b/modbus_registers/shelly_pro_3m_registers.xlsx similarity index 100% rename from data/shelly_pro_3m_registers.xlsx rename to modbus_registers/shelly_pro_3m_registers.xlsx diff --git a/shelly_pro_3m.py b/shelly_pro_3m.py index 3bd527d..d1cee53 100644 --- a/shelly_pro_3m.py +++ b/shelly_pro_3m.py @@ -18,9 +18,9 @@ class ShellyPro3m: self.client = ModbusTcpClient(self.ip, port=port) try: if not self.client.connect(): - print("Verbindung zur Wärmepumpe fehlgeschlagen.") + print("Verbindung zum Shelly-Logger fehlgeschlagen.") exit(1) - print("Verbindung zur Wärmepumpe erfolgreich.") + print("Verbindung zum Shelly-Logger erfolgreich.") except KeyboardInterrupt: print("Beendet durch Benutzer (Ctrl+C).") finally: @@ -28,7 +28,7 @@ class ShellyPro3m: def get_registers(self): # Excel-Datei mit den Input-Registerinformationen - excel_path = "data/shelly_pro_3m_registers.xlsx" + excel_path = "modbus_registers/shelly_pro_3m_registers.xlsx" xls = pd.ExcelFile(excel_path) df_input_registers = xls.parse() @@ -45,7 +45,7 @@ class ShellyPro3m: for _, row in df_clean.iterrows() } - def get_data(self): + def get_state(self): data = {} data['Zeit'] = time.strftime('%Y-%m-%d %H:%M:%S') for address, info in self.registers.items(): diff --git a/test_wr.py b/test_wr.py new file mode 100644 index 0000000..4da815f --- /dev/null +++ b/test_wr.py @@ -0,0 +1,110 @@ +from pymodbus.client import ModbusTcpClient +import struct +import sys +from typing import Optional + +# === Verbindungseinstellungen === +MODBUS_IP = "192.168.1.107" +MODBUS_PORT = 502 # SetApp: 1502; LCD-Menü: 502 -> ggf. anpassen +UNIT_ID = 1 # Default laut Doku: 1 + +client = ModbusTcpClient(MODBUS_IP, port=MODBUS_PORT) +if not client.connect(): + print("Verbindung fehlgeschlagen.") + sys.exit(1) + +def read_regs(addr: int, count: int): + """Hilfsfunktion: liest 'count' Holding-Register ab base-0 'addr'.""" + rr = client.read_holding_registers(address=addr, count=count) + if rr.isError(): + return None + return rr.registers + +def read_string(addr: int, words: int) -> Optional[str]: + """ + SunSpec-Strings: ASCII, Big-Endian, 2 Bytes pro Register, 0x00 gepadded. + """ + regs = read_regs(addr, words) + if regs is None: + return None + b = b"".join(struct.pack(">H", r) for r in regs) + # SunSpec Strings sind meist mit \x00 und Spaces gepadded: + s = b.decode("ascii", errors="ignore").rstrip("\x00 ").strip() + return s or None + +def to_int16(u16: int) -> int: + """unsigned 16 -> signed 16""" + return struct.unpack(">h", struct.pack(">H", u16))[0] + +def apply_sf(raw: int, sf: int) -> float: + return raw * (10 ** sf) + +def read_scaled(value_addr: int, sf_addr: int) -> Optional[float]: + regs = read_regs(value_addr, 1) + sf = read_regs(sf_addr, 1) + if regs is None or sf is None: + return None + raw = to_int16(regs[0]) + sff = to_int16(sf[0]) + return apply_sf(raw, sff) + +def read_u32_with_sf(value_addr: int, sf_addr: int) -> Optional[float]: + """ + Liest 32-bit Zähler (acc32, Big-Endian, 2 Register) + SF. + """ + regs = read_regs(value_addr, 2) + sf = read_regs(sf_addr, 1) + if regs is None or sf is None: + return None + # Big-Endian zusammenbauen: + u32 = (regs[0] << 16) | regs[1] + sff = to_int16(sf[0]) + return apply_sf(u32, sff) + +# ==== Common Block (base-0) ==== +manufacturer = read_string(40004, 16) # C_Manufacturer +model = read_string(40020, 16) # C_Model +version = read_string(40044, 8) # C_Version +serial = read_string(40052, 16) # C_SerialNumber + +print(f"Hersteller: {manufacturer}") +print(f"Modell: {model}") +print(f"Version: {version}") +print(f"Seriennummer: {serial}") + +# ==== Inverter Block (base-0) ==== +# AC Power + Scale Factor +ac_power = read_scaled(40083, 40084) # I_AC_Power, I_AC_Power_SF +if ac_power is not None: + print(f"AC Power: {ac_power} W") +else: + print("Fehler beim Lesen von AC Power") + +# AC Spannung L-N Durchschnitt (falls 1ph/3ph mit N verfügbar) + SF +ac_voltage = read_scaled(40079, 40082) # I_AC_VoltageAN, I_AC_Voltage_SF +if ac_voltage is not None: + print(f"AC Spannung: {ac_voltage} V") + +# AC Frequenz + SF +ac_freq = read_scaled(40085, 40086) # I_AC_Frequency, _SF +if ac_freq is not None: + print(f"Frequenz: {ac_freq} Hz") + +# DC Power + SF +dc_power = read_scaled(40100, 40101) # I_DC_Power, _SF +if dc_power is not None: + print(f"DC Power: {dc_power} W") + +# Lifetime Energy (AC_Energy_WH, acc32) + SF +lifetime_wh = read_u32_with_sf(40093, 40095) # I_AC_Energy_WH, _SF +if lifetime_wh is not None: + print(f"Lifetime Energy: {lifetime_wh} Wh") + +# Status +status_regs = read_regs(40107, 2) # I_Status, I_Status_Vendor +if status_regs: + i_status = status_regs[0] + i_status_vendor = status_regs[1] + print(f"Status: {i_status} (Vendor: {i_status_vendor})") + +client.close()