Cool, kann damit am WE mal ein bisschen rumprobieren.
Ist ja nicht schlimm, wenn wir es besser machen als die Mitbewerber.

Wenn jemand über das SolarEdge Portal dann beispielsweise den Speicher laden möchte o.ä., funktioniert das nur, wenn der Control-Mode wieder freigegeben ist (2).
Ich habe noch folgende Beobachtungen gemacht:
- Ich habe nicht, wie in dem Dokument beschrieben die Werte für AdvancedPwrControlEn und ReactivePwrConfig verändert, es hat trotzdem funktioniert.
- Ich habe auch nie den Commit Befehl ausgeführt, ich hatte das so verstanden, dass das nur für das setzen von AdvancedPwrControlEn und ReactivePwrConfig nötig ist.
Ich weiß zwar nicht wirklich wie das funktioniert, habe in Deinem Code aber mal die Registervariablen an die Bezeichnungen von SolarEdge aus dem Dokument angepasst, suchen/ersetzen kriege ich noch hin ohne den Rest zu verstehen.
Code: Alles auswählen
#!/usr/bin/env python3
import logging
from typing import Dict, Tuple, Union, Optional
from pymodbus.constants import Endian
from dataclass_utils import dataclass_from_dict
from modules.common import modbus
from modules.common.abstract_device import AbstractBat
from modules.common.component_state import BatState
from modules.common.component_type import ComponentDescriptor
from modules.common.fault_state import ComponentInfo, FaultState
from modules.common.modbus import ModbusDataType
from modules.common.simcount import SimCounter
from modules.common.store import get_bat_value_store
from modules.devices.solaredge.solaredge.config import SolaredgeBatSetup
log = logging.getLogger(__name__)
FLOAT32_UNSUPPORTED = -0xffffff00000000000000000000000000
class SolaredgeBat(AbstractBat):
"""
Klasse zur Verwaltung der Solaredge-Batteriesteuerung.
Beinhaltet Funktionen zur Überwachung und Steuerung.
"""
# Register-Adressen in Dezimal
CONTROL_MODE_REGISTER = 57348 # 0xE004
DISCHARGE_LIMIT_REGISTER = 57360 # 0xE010
# Zusätzliche Register für Command-Mode (7 - Max.Eigenverbrauch)
DEFAULT_MODE_REGISTER = 57354 # 0xE00A
COMMAND_MODE_REGISTER = 57357 # 0xE00D
# Commit Register
COMMIT_REGISTER = 57741 # 0xE005 (Commit)
def __init__(
self,
device_id: int,
component_config: Union[Dict, SolaredgeBatSetup],
tcp_client: modbus.ModbusTcpClient_
) -> None:
"""
Initialisiert die Batteriesteuerung.
"""
self.__device_id = device_id
self.component_config = dataclass_from_dict(SolaredgeBatSetup, component_config)
self.__tcp_client = tcp_client
self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher")
self.store = get_bat_value_store(self.component_config.id)
self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config))
def update(self) -> None:
"""
Aktualisiert den aktuellen Batteriestatus und speichert ihn im Value Store.
"""
try:
self.store.set(self.read_state())
except Exception:
log.exception("Fehler beim Aktualisieren des Batterie-Status")
self.fault_state.set_fault("Batterie-Status konnte nicht aktualisiert werden")
def read_state(self) -> BatState:
"""
Liest den aktuellen Zustand der Batterie und gibt ihn als BatState zurück.
"""
try:
power, soc = self.get_values()
imported, exported = self.get_imported_exported(power)
return BatState(power=power, soc=soc, imported=imported, exported=exported)
except Exception:
log.exception("Fehler beim Lesen des Batterie-Zustands")
self.fault_state.set_fault("Fehler beim Lesen des Batterie-Zustands")
return BatState(power=0, soc=0, imported=0, exported=0)
def get_values(self) -> Tuple[float, float]:
"""
Liest SOC und Leistung aus den entsprechenden Modbus-Registern.
"""
unit = self.component_config.configuration.modbus_id
try:
soc = self.__tcp_client.read_holding_registers(
62852, ModbusDataType.FLOAT_32, wordorder=Endian.Little, unit=unit
)
power = self.__tcp_client.read_holding_registers(
62836, ModbusDataType.FLOAT_32, wordorder=Endian.Little, unit=unit
)
if power is None or len(power) == 0 or power[0] == FLOAT32_UNSUPPORTED:
power = 0
return power, soc
except Exception:
log.exception("Fehler beim Abrufen der Werte aus den Registern")
return 0, 0
def get_imported_exported(self, power: float) -> Tuple[float, float]:
"""
Berechnet importierte und exportierte Energie basierend auf der aktuellen Leistung.
"""
return self.sim_counter.sim_count(power)
def ensure_remote_control_mode(self, unit: int) -> bool:
"""
Aktiviert den Remote Control Modus, falls nicht bereits aktiv.
"""
try:
current_mode = self.__tcp_client.read_holding_registers(
self.CONTROL_MODE_REGISTER, ModbusDataType.INT_16, unit=unit
)
if current_mode and len(current_mode) > 0 and current_mode[0] == 4:
log.debug("Remote control mode is already enabled.")
return True
log.info("Enabling remote control mode.")
self.__tcp_client.write_registers(self.CONTROL_MODE_REGISTER, [4], unit=unit)
self.commit_changes(unit)
return True
except Exception:
log.exception("Error enabling remote control mode")
return False
def commit_changes(self, unit: int) -> None:
"""
Bestätigt Änderungen durch Schreiben in das COMMIT-Register.
"""
try:
self.__tcp_client.write_registers(self.COMMIT_REGISTER, [1], unit=unit)
except Exception:
log.exception("Error committing changes")
def set_power_limit(self, power_limit: Optional[Union[int, float]]) -> None:
"""
Setzt das Entladeleistungs-Limit der Batterie innerhalb eines gültigen Bereichs.
"""
unit = self.component_config.configuration.modbus_id
if not self.ensure_remote_control_mode(unit):
return
if power_limit is None:
power_limit = 5000
if power_limit < 0 or power_limit > 5000:
log.error(f"Invalid discharge limit: {power_limit}. Must be between 0 and 5000.")
return
try:
self.__tcp_client.write_registers(self.DISCHARGE_LIMIT_REGISTER, [int(power_limit)], unit=unit)
self.commit_changes(unit)
except Exception:
log.exception("Error setting discharge limit")
def set_storage_mode(self, mode: str, discharge_limit: Optional[Union[int, float]] = None) -> None:
"""
Setzt den gewünschten Speicherbetriebsmodus.
Modi:
1. "immer": Kein Eingriff, ggf. Register 0xE004 auf 2 (Time of Use) setzen.
2. "gesperrt": (Gesperrt, wenn Fahrzeug lädt)
- 0xE004 auf 4 (Remote)
- 0xE00A auf 7 (Max.Eigenverbrauch)
- 0xE00D auf 7 (Max.Eigenverbrauch)
- 0xE010 auf 0 (keine Entladung)
3. "hausverbrauch": (Für Hausverbrauch)
- 0xE004 auf 4 (Remote)
- 0xE00A auf 7 (Max.Eigenverbrauch)
- 0xE00D auf 7 (Max.Eigenverbrauch)
- 0xE010 auf den aktuellen Hausverbrauch in Watt (Entladebegrenzung)
Wenn openWB die Steuerung beendet, sollte über release_storage_control() der Modus
wieder auf 2 (Time of Use) zurückgesetzt werden.
"""
unit = self.component_config.configuration.modbus_id
try:
if mode.lower() == "immer":
# openWB greift nicht in die Steuerung ein – sicherheitshalber Time of Use aktivieren.
self.__tcp_client.write_registers(self.CONTROL_MODE_REGISTER, [2], unit=unit)
self.commit_changes(unit)
log.info("Modus 'Immer' gesetzt: Steuerung nicht aktiv, Time of Use.")
elif mode.lower() == "gesperrt":
# Remote-Modus aktivieren und Profile auf Max.Eigenverbrauch setzen.
self.__tcp_client.write_registers(self.CONTROL_MODE_REGISTER, [4], unit=unit)
self.commit_changes(unit)
self.__tcp_client.write_registers(self.DEFAULT_MODE_REGISTER, [7], unit=unit)
self.commit_changes(unit)
self.__tcp_client.write_registers(self.COMMAND_MODE_REGISTER, [7], unit=unit)
self.commit_changes(unit)
# Entladung sperren: 0 Watt
self.__tcp_client.write_registers(self.DISCHARGE_LIMIT_REGISTER, [0], unit=unit)
self.commit_changes(unit)
log.info("Modus 'Gesperrt, wenn Fahrzeug lädt' gesetzt.")
elif mode.lower() == "hausverbrauch":
# Remote-Modus aktivieren und Profile auf Max.Eigenverbrauch setzen.
self.__tcp_client.write_registers(self.CONTROL_MODE_REGISTER, [4], unit=unit)
self.commit_changes(unit)
self.__tcp_client.write_registers(self.DEFAULT_MODE_REGISTER, [7], unit=unit)
self.commit_changes(unit)
self.__tcp_client.write_registers(self.COMMAND_MODE_REGISTER, [7], unit=unit)
self.commit_changes(unit)
# Entladebegrenzung auf den aktuellen Hausverbrauch setzen
if discharge_limit is None:
log.error("Für den 'Hausverbrauch'-Modus muss eine Entladebegrenzung angegeben werden.")
return
self.__tcp_client.write_registers(self.DISCHARGE_LIMIT_REGISTER, [int(discharge_limit)], unit=unit)
self.commit_changes(unit)
log.info("Modus 'Für Hausverbrauch' gesetzt.")
else:
log.error(f"Unbekannter Modus: {mode}")
except Exception:
log.exception("Fehler beim Setzen des Speicherbetriebsmodus")
def release_storage_control(self) -> None:
"""
Beendet die Steuerung durch openWB, indem der Modus auf Time of Use (Wert 2) gesetzt wird.
Dadurch kann das SolarEdge Monitoring-Portal seine Profile (z.B. Max.Eigenverbrauch) nutzen.
"""
unit = self.component_config.configuration.modbus_id
try:
self.__tcp_client.write_registers(self.CONTROL_MODE_REGISTER, [2], unit=unit)
self.commit_changes(unit)
log.info("openWB-Steuerung beendet, Modus auf Time of Use (2) gesetzt.")
except Exception:
log.exception("Fehler beim Freigeben der Speichersteuerung")
component_descriptor = ComponentDescriptor(configuration_factory=SolaredgeBatSetup)
Dann habe ich noch folgende Fragen:
Wie oft wird denn der Command Mode gesetzt?
Läuft das nur einmal am Anfang eines Ladevorganges / der Speichersteuerung oder regelmäßig?
Im Standard ist der Timeout auf 1h (3600sec) gesetzt, danach wird der Default Mode genommen.
Die sind bei uns zwar identisch, ich weiß aber nicht, ob noch mehr nach Ablauf des Timeouts passiert, was dann stören könnte.
P.S: Auf Anhieb klappt es nicht gleich:
Code: Alles auswählen
2025-02-28 20:44:22,221 - {modules.devices.solaredge.solaredge.bat:91} - {ERROR:device0} - Fehler beim Abrufen der Werte aus den Registern
Traceback (most recent call last):
File "/var/www/html/openWB/packages/modules/devices/solaredge/solaredge/bat.py", line 87, in get_values
if power is None or len(power) == 0 or power[0] == FLOAT32_UNSUPPORTED:
TypeError: object of type 'float' has no len()
...
2025-02-28 20:44:23,277 - {modules.devices.solaredge.solaredge.bat:116} - {ERROR:set power limit 4} - Error enabling remote control mode
Traceback (most recent call last):
File "/var/www/html/openWB/packages/modules/common/modbus.py", line 106, in __read_registers
raise Exception(__name__+" "+str(response))
Exception: modules.common.modbus Modbus Error: [Input/Output] [Errno 104] Connection reset by peer
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/var/www/html/openWB/packages/modules/devices/solaredge/solaredge/bat.py", line 105, in ensure_remote_control_mode
current_mode = self.__tcp_client.read_holding_registers(
File "/var/www/html/openWB/packages/modules/common/modbus.py", line 138, in read_holding_registers
return self.__read_registers(
File "/var/www/html/openWB/packages/modules/common/modbus.py", line 121, in __read_registers
raise Exception(__name__+" "+str(type(e))+" " + str(e)) from e
Exception: modules.common.modbus <class 'Exception'> modules.common.modbus Modbus Error: [Input/Output] [Errno 104] Connection reset by peer