still not able to connect to slave

This commit is contained in:
Nils Reiners
2025-10-23 14:00:44 +02:00
parent 5827b494b5
commit 38116390df
2 changed files with 72 additions and 74 deletions

84
main.py
View File

@@ -26,57 +26,57 @@ db = DataBaseInflux(
bucket="allmende_db" bucket="allmende_db"
) )
hp_master = HeatPump(device_name='hp_master', ip_address='10.0.0.10', port=502) # hp_master = HeatPump(device_name='hp_master', ip_address='10.0.0.10', port=502)
hp_slave = HeatPump(device_name='hp_slave', ip_address='10.0.0.11', port=502) # hp_slave = HeatPump(device_name='hp_slave', ip_address='10.0.0.11', port=502)
shelly = ShellyPro3m(device_name='wohnung_2_6', ip_address='192.168.1.121') # shelly = ShellyPro3m(device_name='wohnung_2_6', ip_address='192.168.1.121')
wr_master = PvInverter(device_name='solaredge_master', ip_address='192.168.1.112', unit=1) wr_master = PvInverter(device_name='solaredge_master', ip_address='192.168.1.112', unit=1)
wr_slave = PvInverter(device_name='solaredge_slave', ip_address='192.168.1.112', unit=3) wr_slave = PvInverter(device_name='solaredge_slave', ip_address='192.168.1.112', unit=3)
meter = SolaredgeMeter(device_name='solaredge_meter', ip_address='192.168.1.112') meter = SolaredgeMeter(device_name='solaredge_meter', ip_address='192.168.1.112')
es.add_components(hp_master, hp_slave, shelly, wr_master, wr_slave, meter) es.add_components(wr_master, wr_slave)#hp_master, hp_slave, shelly, wr_master, wr_slave, meter)
controller = SgReadyController(es) # controller = SgReadyController(es)
#
# FORECASTING # # FORECASTING
latitude = 48.041 # latitude = 48.041
longitude = 7.862 # longitude = 7.862
TZ = "Europe/Berlin" # TZ = "Europe/Berlin"
HORIZON_DAYS = 2 # HORIZON_DAYS = 2
weather_forecaster = WeatherForecaster(latitude=latitude, longitude=longitude) # weather_forecaster = WeatherForecaster(latitude=latitude, longitude=longitude)
site = Location(latitude=latitude, longitude=longitude, altitude=35, tz=TZ, name="Gundelfingen") # site = Location(latitude=latitude, longitude=longitude, altitude=35, tz=TZ, name="Gundelfingen")
#
p_module = 435 # p_module = 435
upper_roof_north = PvWattsSubarrayConfig(name="north", pdc0_w=(29+29+21)*p_module, tilt_deg=10, azimuth_deg=20, dc_loss=0.02, ac_loss=0.01) # upper_roof_north = PvWattsSubarrayConfig(name="north", pdc0_w=(29+29+21)*p_module, tilt_deg=10, azimuth_deg=20, dc_loss=0.02, ac_loss=0.01)
upper_roof_south = PvWattsSubarrayConfig(name="south", pdc0_w=(29+21+20)*p_module, tilt_deg=10, azimuth_deg=200, dc_loss=0.02, ac_loss=0.01) # upper_roof_south = PvWattsSubarrayConfig(name="south", pdc0_w=(29+21+20)*p_module, tilt_deg=10, azimuth_deg=200, dc_loss=0.02, ac_loss=0.01)
upper_roof_east = PvWattsSubarrayConfig(name="east", pdc0_w=7*p_module, tilt_deg=10, azimuth_deg=110, dc_loss=0.02, ac_loss=0.01) # upper_roof_east = PvWattsSubarrayConfig(name="east", pdc0_w=7*p_module, tilt_deg=10, azimuth_deg=110, dc_loss=0.02, ac_loss=0.01)
upper_roof_west = PvWattsSubarrayConfig(name="west", pdc0_w=7*p_module, tilt_deg=10, azimuth_deg=290, dc_loss=0.02, ac_loss=0.01) # upper_roof_west = PvWattsSubarrayConfig(name="west", pdc0_w=7*p_module, tilt_deg=10, azimuth_deg=290, dc_loss=0.02, ac_loss=0.01)
cfgs = [upper_roof_north, upper_roof_south, upper_roof_east, upper_roof_west] # cfgs = [upper_roof_north, upper_roof_south, upper_roof_east, upper_roof_west]
pv_plant = PvWattsPlant(site, cfgs) # pv_plant = PvWattsPlant(site, cfgs)
#
now = datetime.now() # now = datetime.now()
next_forecast_at = (now + dt.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0) # next_forecast_at = (now + dt.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0)
while True: while True:
now = datetime.now() now = datetime.now()
if now.second % interval_seconds == 0 and now.microsecond < 100_000: if now.second % interval_seconds == 0 and now.microsecond < 100_000:
state = es.get_state_and_store_to_database(db) state = es.get_state_and_store_to_database(db)
mode = controller.perform_action(heat_pump_name='hp_master', meter_name='solaredge_meter', state=state) # mode = controller.perform_action(heat_pump_name='hp_master', meter_name='solaredge_meter', state=state)
#
# if mode == 'mode1':
# mode_as_binary = 0
# else:
# mode_as_binary = 1
# db.store_data('sg_ready', {'mode': mode_as_binary})
if mode == 'mode1': # if now >= next_forecast_at:
mode_as_binary = 0 # # Start der Prognose: ab der kommenden vollen Stunde
else: # start_hour_local = (now + dt.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0)
mode_as_binary = 1 # weather = weather_forecaster.get_hourly_forecast(start_hour_local, HORIZON_DAYS)
db.store_data('sg_ready', {'mode': mode_as_binary}) # total = pv_plant.get_power(weather)
# db.store_forecasts('pv_forecast', total)
if now >= next_forecast_at: #
# Start der Prognose: ab der kommenden vollen Stunde # # Nächste geplante Ausführung definieren (immer volle Stunde)
start_hour_local = (now + dt.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0) # # Falls wir durch Delay mehrere Stunden verpasst haben, hole auf:
weather = weather_forecaster.get_hourly_forecast(start_hour_local, HORIZON_DAYS) # while next_forecast_at <= now:
total = pv_plant.get_power(weather) # next_forecast_at = (next_forecast_at + dt.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0)
db.store_forecasts('pv_forecast', total)
# Nächste geplante Ausführung definieren (immer volle Stunde)
# Falls wir durch Delay mehrere Stunden verpasst haben, hole auf:
while next_forecast_at <= now:
next_forecast_at = (next_forecast_at + dt.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0)
time.sleep(0.1) time.sleep(0.1)

View File

@@ -1,32 +1,40 @@
import time import time
import struct import struct
import pandas as pd import pandas as pd
from typing import Dict, Any, List, Tuple, Optional from typing import Dict, Any, List, Optional
from pymodbus.client import ModbusTcpClient from pymodbus.client import ModbusTcpClient
EXCEL_PATH = "modbus_registers/pv_inverter_registers.xlsx" EXCEL_PATH = "modbus_registers/pv_inverter_registers.xlsx"
# Obergrenze: bis EXKLUSIVE 40206 (d.h. max. 40205) # Bis EXKLUSIVE 40206 (also max. 40205)
MAX_ADDR_EXCLUSIVE = 40121 MAX_ADDR_EXCLUSIVE = 40206
class PvInverter: class PvInverter:
def __init__(self, device_name: str, ip_address: str, port: int = 502, unit: int = 1): def __init__(self, device_name: str, ip_address: str, port: int = 502, unit: int = 1):
"""
device_name : Anzeigename (z.B. 'master' oder 'slave')
ip_address : IP des Wechselrichters oder Modbus-Gateways
port : TCP-Port (Standard 502)
unit : Modbus Unit-ID (1 = Master, 3 = Slave)
"""
self.device_name = device_name self.device_name = device_name
self.ip = ip_address self.ip = ip_address
self.port = port self.port = port
self.unit = unit self.unit = unit
self.client: Optional[ModbusTcpClient] = None self.client: Optional[ModbusTcpClient] = None
self.registers: Dict[int, Dict[str, Any]] = {} # addr -> {"desc":..., "type":...} self.registers: Dict[int, Dict[str, Any]] = {}
self.connect_to_modbus() self.connect_to_modbus()
self.load_registers(EXCEL_PATH) self.load_registers(EXCEL_PATH)
# ---------- Verbindung ---------- # ---------- Verbindung ----------
def connect_to_modbus(self): def connect_to_modbus(self):
self.client = ModbusTcpClient(self.ip, port=self.port, timeout=3.0, retries=3) self.client = ModbusTcpClient(self.ip, port=self.port, timeout=3.0)
if not self.client.connect(): if not self.client.connect():
print("❌ Verbindung zu Wechselrichter fehlgeschlagen.") print(f"❌ Verbindung zu {self.device_name} ({self.ip}:{self.port}) fehlgeschlagen.")
raise SystemExit(1) raise SystemExit(1)
print("✅ Verbindung zu Wechselrichter hergestellt.") print(f"✅ Verbindung hergestellt zu {self.device_name} ({self.ip}:{self.port}, unit={self.unit})")
def close(self): def close(self):
if self.client: if self.client:
@@ -37,12 +45,13 @@ class PvInverter:
def load_registers(self, excel_path: str): def load_registers(self, excel_path: str):
xls = pd.ExcelFile(excel_path) xls = pd.ExcelFile(excel_path)
df = xls.parse() df = xls.parse()
# Passe Spaltennamen hier an, falls nötig:
# Passe Spaltennamen an deine Excel an
cols = ["MB Adresse", "Beschreibung", "Variabel Typ"] cols = ["MB Adresse", "Beschreibung", "Variabel Typ"]
df = df[cols].dropna() df = df[cols].dropna()
df["MB Adresse"] = df["MB Adresse"].astype(int) df["MB Adresse"] = df["MB Adresse"].astype(int)
# 1) Vorab-Filter: nur Adressen < 40206 übernehmen # Nur Register unterhalb der Grenze übernehmen
df = df[df["MB Adresse"] < MAX_ADDR_EXCLUSIVE] df = df[df["MB Adresse"] < MAX_ADDR_EXCLUSIVE]
self.registers = { self.registers = {
@@ -53,21 +62,16 @@ class PvInverter:
for _, row in df.iterrows() for _, row in df.iterrows()
} }
# ---------- Low-Level Lesen ---------- # ---------- Low-Level Lesen ----------
def _try_read(self, fn_name: str, address: int, count: int) -> Optional[List[int]]: def _try_read(self, fn_name: str, address: int, count: int) -> Optional[List[int]]:
"""
Ruft die pymodbus-Funktion mit fester unit-ID auf (kein Fallback).
"""
fn = getattr(self.client, fn_name) fn = getattr(self.client, fn_name)
# pymodbus 3.8.x hat 'slave='; Fallbacks schaden nicht res = fn(address=address, count=count, slave=self.unit)
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()): if res is None or (hasattr(res, "isError") and res.isError()):
continue
return res.registers
except TypeError:
continue
return None return None
return getattr(res, "registers", None)
def _read_any(self, address: int, count: int) -> Optional[List[int]]: def _read_any(self, address: int, count: int) -> Optional[List[int]]:
regs = self._try_read("read_holding_registers", address, count) regs = self._try_read("read_holding_registers", address, count)
@@ -85,34 +89,30 @@ class PvInverter:
b = struct.pack(">HH", u16_hi, u16_lo) if msw_first else struct.pack(">HH", u16_lo, u16_hi) b = struct.pack(">HH", u16_hi, u16_lo) if msw_first else struct.pack(">HH", u16_lo, u16_hi)
return struct.unpack(">f", b)[0] return struct.unpack(">f", b)[0]
# Hilfsfunktion: wie viele 16-Bit-Register braucht dieser Typ? # Wie viele Register braucht der Typ?
@staticmethod @staticmethod
def _word_count_for_type(rtype: str) -> int: def _word_count_for_type(rtype: str) -> int:
rt = (rtype or "").lower() rt = (rtype or "").lower()
# Passe hier an deine Excel-Typen an:
if "uint32" in rt or "real" in rt or "float" in rt or "string(32)" in rt: if "uint32" in rt or "real" in rt or "float" in rt or "string(32)" in rt:
return 2 return 2
# Default: 1 Wort (z.B. int16/uint16)
return 1 return 1
# ---------- Lesen ----------
def read_one(self, address_excel: int, rtype: str) -> Optional[float]: def read_one(self, address_excel: int, rtype: str) -> Optional[float]:
""" """
Liest einen Wert nach Typ ('INT' oder 'REAL' etc.). Liest einen Wert nach Typ ('INT', 'REAL' etc.) mit fixer Unit-ID.
Es werden ausschließlich Register < 40206 gelesen.
""" """
addr = int(address_excel) addr = int(address_excel)
words = self._word_count_for_type(rtype) words = self._word_count_for_type(rtype)
# 2) Harte Grenze prüfen: höchstes angefasstes Register muss < 40206 sein # Grenze prüfen
if addr + words - 1 >= MAX_ADDR_EXCLUSIVE: if addr + words - 1 >= MAX_ADDR_EXCLUSIVE:
# Überspringen, da der Lesevorgang die Grenze >= 40206 berühren würde
return None return None
if words == 2: if words == 2:
regs = self._read_any(addr, 2) regs = self._read_any(addr, 2)
if not regs or len(regs) < 2: if not regs or len(regs) < 2:
return None return None
# Deine bisherige Logik interpretiert 2 Worte als Float32:
return self._to_f32_from_two(regs[0], regs[1]) return self._to_f32_from_two(regs[0], regs[1])
else: else:
regs = self._read_any(addr, 1) regs = self._read_any(addr, 1)
@@ -122,13 +122,11 @@ class PvInverter:
def get_state(self) -> Dict[str, Any]: def get_state(self) -> Dict[str, Any]:
""" """
Liest ALLE Register aus self.registers und gibt dict zurück. Liest alle gültigen Register und gibt ein dict zurück.
Achtet darauf, dass keine Adresse (inkl. Mehrwort) >= 40206 gelesen wird.
""" """
data = {"Zeit": time.strftime("%Y-%m-%d %H:%M:%S")} data = {"Zeit": time.strftime("%Y-%m-%d %H:%M:%S")}
for address, meta in sorted(self.registers.items()): for address, meta in sorted(self.registers.items()):
words = self._word_count_for_type(meta["type"]) words = self._word_count_for_type(meta["type"])
# 3) Nochmals Schutz auf Ebene der Iteration:
if address + words - 1 >= MAX_ADDR_EXCLUSIVE: if address + words - 1 >= MAX_ADDR_EXCLUSIVE:
continue continue
val = self.read_one(address, meta["type"]) val = self.read_one(address, meta["type"])