From 8eda3bc95459bfe0c093bda88eb91cf574814135 Mon Sep 17 00:00:00 2001 From: Nils Reiners Date: Tue, 16 Sep 2025 22:46:42 +0200 Subject: [PATCH] reading out registers corrected --- __pycache__/data_base_influx.cpython-312.pyc | Bin 1793 -> 1786 bytes __pycache__/heat_pump.cpython-312.pyc | Bin 3666 -> 3659 bytes __pycache__/pv_inverter.cpython-312.pyc | Bin 3680 -> 6960 bytes __pycache__/shelly_pro_3m.cpython-312.pyc | Bin 3819 -> 3812 bytes main.py | 6 +- pv_inverter.py | 123 +++++++++++++------ test_meter.py | 61 +++++++++ 7 files changed, 150 insertions(+), 40 deletions(-) create mode 100644 test_meter.py diff --git a/__pycache__/data_base_influx.cpython-312.pyc b/__pycache__/data_base_influx.cpython-312.pyc index 2cc1072e519f0c51be743464954832daa9198b13..0aa648a43930b642e663258dc5f7343066c02307 100644 GIT binary patch delta 64 zcmZqV`^C$BnwOW00SNe=PHyDRW>T}%&&bbB)z8b!Db{z%PcF?(%_}L^PtPpTPt3^y Ra#B*`Q*(^cjpIn-onpaY+pPpHwU!Is+ Zl$%;mnp=>X7oU@#o}OB?c@9$?3ji;k7?uD4 diff --git a/__pycache__/heat_pump.cpython-312.pyc b/__pycache__/heat_pump.cpython-312.pyc index 6cb73db44f89071eb7847918b6ff4d82d369378d..704be12c106b05e39f761370606f0801bb18696f 100644 GIT binary patch delta 42 wcmca4b6SS$G%qg~0}${#ZRGmO$Y?Z)Ns=cqCnq;GFC{fTHMe-P4wD)W0P|`Kwg3PC delta 49 zcmX>tb4iBlG%qg~0}!MOZshvP$Y?o>{lNt{I DVloc% diff --git a/__pycache__/pv_inverter.cpython-312.pyc b/__pycache__/pv_inverter.cpython-312.pyc index 9cccd2daf7c43a6621fdc71e8f9ae6aa1abf848e..25174a8e73d8adfadee6ccfe8c13f9f21ebb1c6f 100644 GIT binary patch literal 6960 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 literal 3680 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 diff --git a/__pycache__/shelly_pro_3m.cpython-312.pyc b/__pycache__/shelly_pro_3m.cpython-312.pyc index c74d295fb4bbf5d3deaa4cf128fa4022a67c882e..036f04184ce7160270913b66003a619e4ce3cad8 100644 GIT binary patch delta 42 wcmaDY`$U%OG%qg~0}${#E#zWiG@H!LB*~MQlarg8my#Nvnp?ctoM{UW0Og+xj{pDw delta 49 zcmaDN`&yRkG%qg~0}!MOF63fjw4Kb&Bq>~;m|B#ZT2Pu>keU~tlb@cRTC~}KX$ubk DPc9D3 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()