Files
allmende_ems/pv_inverter.py
Nils Reiners 397935f51a minor changes
2025-09-16 22:55:13 +02:00

115 lines
4.4 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import time
import struct
import pandas as pd
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: Optional[ModbusTcpClient] = None
self.registers: Dict[int, Dict[str, Any]] = {} # addr -> {"desc":..., "type":...}
self.connect_to_modbus()
self.load_registers(EXCEL_PATH)
# ---------- Verbindung ----------
def connect_to_modbus(self):
self.client = ModbusTcpClient(self.ip, port=self.port, timeout=3.0, retries=3)
if not self.client.connect():
print("❌ Verbindung zu Wechselrichter fehlgeschlagen.")
raise SystemExit(1)
print("✅ Verbindung zu Wechselrichter hergestellt.")
def close(self):
if self.client:
self.client.close()
self.client = None
# ---------- Register-Liste ----------
def load_registers(self, excel_path: str):
xls = pd.ExcelFile(excel_path)
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 = {
int(row["MB Adresse"]): {
"desc": str(row["Beschreibung"]).strip(),
"type": norm_type(row["Variabel Typ"])
}
for _, row in df.iterrows()
}
print(f" {len(self.registers)} Register aus Excel geladen.")
# ---------- 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
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