diff --git a/__pycache__/data_base_influx.cpython-312.pyc b/__pycache__/data_base_influx.cpython-312.pyc index 2cc1072..0aa648a 100644 Binary files a/__pycache__/data_base_influx.cpython-312.pyc and b/__pycache__/data_base_influx.cpython-312.pyc differ diff --git a/__pycache__/heat_pump.cpython-312.pyc b/__pycache__/heat_pump.cpython-312.pyc index 6cb73db..704be12 100644 Binary files a/__pycache__/heat_pump.cpython-312.pyc and b/__pycache__/heat_pump.cpython-312.pyc differ diff --git a/__pycache__/pv_inverter.cpython-312.pyc b/__pycache__/pv_inverter.cpython-312.pyc index 9cccd2d..25174a8 100644 Binary files a/__pycache__/pv_inverter.cpython-312.pyc and b/__pycache__/pv_inverter.cpython-312.pyc differ diff --git a/__pycache__/shelly_pro_3m.cpython-312.pyc b/__pycache__/shelly_pro_3m.cpython-312.pyc index c74d295..036f041 100644 Binary files a/__pycache__/shelly_pro_3m.cpython-312.pyc and b/__pycache__/shelly_pro_3m.cpython-312.pyc differ diff --git a/main.py b/main.py index 1df6135..84a03ce 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,12 @@ import time from datetime import datetime -from data_base_csv import DataBaseCsv from data_base_influx import DataBaseInflux from heat_pump import HeatPump from pv_inverter import PvInverter from shelly_pro_3m import ShellyPro3m # For dev-System run in terminal: ssh -N -L 127.0.0.1:8111:10.0.0.10:502 pi@192.168.1.146 -# For productive-System change port in heatpump to 502 +# For productive-System change IP-adress in heatpump to '10.0.0.10' and port to 502 interval_seconds = 10 @@ -18,7 +17,7 @@ db = DataBaseInflux( bucket="allmende_db" ) -hp = HeatPump(device_name='hp_master', ip_address='localhost', port=8111) +hp = HeatPump(device_name='hp_master', ip_address='10.0.0.10', port=502) shelly = ShellyPro3m(device_name='wohnung_2_6', ip_address='192.168.1.121') wr = PvInverter(device_name='wr_master', ip_address='192.168.1.112') @@ -31,5 +30,6 @@ while True: 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/pv_inverter.py b/pv_inverter.py index 365d7fb..705eb27 100644 --- a/pv_inverter.py +++ b/pv_inverter.py @@ -1,68 +1,117 @@ import time +import struct import pandas as pd +import matplotlib.pyplot as plt +from collections import deque +from typing import Dict, Any, List, Tuple, Optional from pymodbus.client import ModbusTcpClient +EXCEL_PATH = "modbus_registers/pv_inverter_registers.xlsx" + class PvInverter: def __init__(self, device_name: str, ip_address: str, port: int = 502, unit: int = 1): self.device_name = device_name self.ip = ip_address self.port = port self.unit = unit - self.client = None - self.registers = None - + self.client: Optional[ModbusTcpClient] = None + self.registers: Dict[int, Dict[str, Any]] = {} # addr -> {"desc":..., "type":...} self.connect_to_modbus() - self.get_registers() + self.load_registers(EXCEL_PATH) + # ---------- Verbindung ---------- def connect_to_modbus(self): - # Timeout & retries optional, aber hilfreich: self.client = ModbusTcpClient(self.ip, port=self.port, timeout=3.0, retries=3) if not self.client.connect(): - print("Verbindung zu Wechselrichter fehlgeschlagen.") + print("❌ Verbindung zu Wechselrichter fehlgeschlagen.") raise SystemExit(1) - print("Verbindung zu Wechselrichter erfolgreich.") - - # WICHTIG: NICHT hier schließen! - # finally: self.client.close() <-- entfernen + print("✅ Verbindung zu Wechselrichter hergestellt.") def close(self): if self.client: self.client.close() self.client = None - def get_registers(self): - excel_path = "modbus_registers/pv_inverter_registers.xlsx" + # ---------- Register-Liste ---------- + def load_registers(self, excel_path: str): xls = pd.ExcelFile(excel_path) - df_input_registers = xls.parse() - df_clean = df_input_registers[['MB Adresse', 'Beschreibung', 'Variabel Typ']].dropna() - df_clean['MB Adresse'] = df_clean['MB Adresse'].astype(int) - + df = xls.parse() + # Passen die Spaltennamen bei dir anders, bitte hier anpassen: + cols = ["MB Adresse", "Beschreibung", "Variabel Typ"] + for c in cols: + if c not in df.columns: + raise ValueError(f"Spalte '{c}' fehlt in {excel_path}") + df = df[cols].dropna() + df["MB Adresse"] = df["MB Adresse"].astype(int) + # NORMALISIERE TYP + def norm_type(x: Any) -> str: + s = str(x).strip().upper() + return "REAL" if s == "REAL" else "INT" self.registers = { - row['MB Adresse']: { - 'desc': row['Beschreibung'], - 'type': 'REAL' if str(row['Variabel Typ']).upper() == 'REAL' else 'INT' + int(row["MB Adresse"]): { + "desc": str(row["Beschreibung"]).strip(), + "type": norm_type(row["Variabel Typ"]) } - for _, row in df_clean.iterrows() + for _, row in df.iterrows() } + print(f"ℹ️ {len(self.registers)} Register aus Excel geladen.") - def get_state(self): - data = {'Zeit': time.strftime('%Y-%m-%d %H:%M:%S')} - for address, info in self.registers.items(): - reg_type = info['type'] - # Unit-ID mitgeben (wichtig bei pymodbus>=3) - result = self.client.read_holding_registers( - address=address, - count=2 if reg_type == 'REAL' else 1, - slave=self.unit # pymodbus 2.x -> 'slave', nicht 'unit' - ) - if result.isError(): - print(f"Fehler beim Lesen von Adresse {address}: {result}") + # ---------- Low-Level Lesen ---------- + def _try_read(self, fn_name: str, address: int, count: int) -> Optional[List[int]]: + fn = getattr(self.client, fn_name) + # pymodbus 3.8.x hat 'slave='; Fallbacks schaden nicht + for kwargs in (dict(address=address, count=count, slave=self.unit), + dict(address=address, count=count), + ): + try: + res = fn(**kwargs) + if res is None or (hasattr(res, "isError") and res.isError()): + continue + return res.registers + except TypeError: continue + return None - # Minimal invasiv: wie bei dir – erstes Register verwenden - value = result.registers[0] - print(f"Adresse {address} - {info['desc']}: {value}") - data[f"{address} - {info['desc']}"] = value - return data + def _read_any(self, address: int, count: int) -> Optional[List[int]]: + regs = self._try_read("read_holding_registers", address, count) + if regs is None: + regs = self._try_read("read_input_registers", address, count) + return regs + # ---------- Decoding ---------- + @staticmethod + def _to_i16(u16: int) -> int: + return struct.unpack(">h", struct.pack(">H", u16))[0] + @staticmethod + def _to_f32_from_two(u16_hi: int, u16_lo: int, msw_first: bool = True) -> float: + if msw_first: + b = struct.pack(">HH", u16_hi, u16_lo) + else: + b = struct.pack(">HH", u16_lo, u16_hi) + return struct.unpack(">f", b)[0] + + def read_one(self, address_excel: int, rtype: str) -> Optional[float]: + """Liest einen Wert nach Typ ('INT' oder 'REAL') unter Berücksichtigung Base-1.""" + addr = address_excel + if rtype == "REAL": + regs = self._read_any(addr, 2) + if not regs or len(regs) < 2: + return None + return self._to_f32_from_two(regs[0], regs[1]) + else: # INT + regs = self._read_any(addr, 1) + if not regs: + return None + return float(self._to_i16(regs[0])) + + def get_state(self) -> Dict[str, Any]: + """Liest ALLE Register aus self.registers und gibt dict zurück.""" + data = {"Zeit": time.strftime("%Y-%m-%d %H:%M:%S")} + for address, meta in self.registers.items(): + val = self.read_one(address, meta["type"]) + if val is None: + continue + key = f"{address} - {meta['desc']}" + data[key] = val + return data \ No newline at end of file diff --git a/test_meter.py b/test_meter.py new file mode 100644 index 0000000..bb90c24 --- /dev/null +++ b/test_meter.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from pymodbus.client import ModbusTcpClient +import struct, sys + +MODBUS_IP = "192.168.1.112" +MODBUS_PORT = 502 +UNIT_ID = 1 + +METER_START = 40240 # Startadresse Model 203-Felder + +def to_i16(u16): # unsigned 16 → signed 16 + return struct.unpack(">h", struct.pack(">H", u16))[0] + +def read_regs(client, addr, count): + rr = client.read_holding_registers(address=addr, count=count, slave=UNIT_ID) + if rr.isError(): + return None + return rr.registers + +def read_meter_power(client): + base = METER_START + p = read_regs(client, base + 16, 1) # M_AC_Power + pa = read_regs(client, base + 17, 1) # Phase A + pb = read_regs(client, base + 18, 1) # Phase B + pc = read_regs(client, base + 19, 1) # Phase C + sf = read_regs(client, base + 20, 1) # Scale Factor + if not p or not sf: + return None + sff = to_i16(sf[0]) + return { + "total": to_i16(p[0]) * (10 ** sff), + "A": to_i16(pa[0]) * (10 ** sff) if pa else None, + "B": to_i16(pb[0]) * (10 ** sff) if pb else None, + "C": to_i16(pc[0]) * (10 ** sff) if pc else None, + "sf": sff + } + +def fmt_w(v): + if v is None: return "-" + neg = v < 0 + v = abs(v) + return f"{'-' if neg else ''}{v/1000:.2f} kW" if v >= 1000 else f"{'-' if neg else ''}{v:.0f} W" + +def main(): + client = ModbusTcpClient(MODBUS_IP, port=MODBUS_PORT) + if not client.connect(): + print("❌ Verbindung fehlgeschlagen."); sys.exit(1) + try: + m = read_meter_power(client) + if m: + print(f"Meter-Leistung: {fmt_w(m['total'])} " + f"(A {fmt_w(m['A'])}, B {fmt_w(m['B'])}, C {fmt_w(m['C'])}) [SF={m['sf']}]") + else: + print("Meter-Leistung konnte nicht gelesen werden.") + finally: + client.close() + +if __name__ == "__main__": + main()