Skip to content

Commit abd0a29

Browse files
authored
SolarEdge Speichersteuerung mit Battery Index (#2269)
* Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update bat.py * Update power_limit sign
1 parent 1f6be33 commit abd0a29

1 file changed

Lines changed: 183 additions & 2 deletions

File tree

  • packages/modules/devices/solaredge/solaredge

packages/modules/devices/solaredge/solaredge/bat.py

Lines changed: 183 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
#!/usr/bin/env python3
22
import logging
3-
from typing import Any, Tuple, TypedDict
3+
4+
from typing import Any, TypedDict, Dict, Union, Optional, Tuple
5+
46

57
from pymodbus.constants import Endian
8+
import pymodbus
9+
10+
11+
from control import data
12+
613

714
from modules.common import modbus
815
from modules.common.abstract_device import AbstractBat
@@ -17,6 +24,11 @@
1724
log = logging.getLogger(__name__)
1825

1926
FLOAT32_UNSUPPORTED = -0xffffff00000000000000000000000000
27+
MAX_DISCHARGE_LIMIT = 5000
28+
DEFAULT_CONTROL_MODE = 1 # Control Mode Max Eigenverbrauch
29+
REMOTE_CONTROL_MODE = 4 # Control Mode Remotesteuerung
30+
DEFAULT_COMMAND_MODE = 0 # Command Mode ohne Steuerung
31+
ACTIVE_COMMAND_MODE = 7 # Command Mode Max Eigenverbrauch bei Steuerung
2032

2133

2234
class KwargsDict(TypedDict):
@@ -25,6 +37,19 @@ class KwargsDict(TypedDict):
2537

2638

2739
class SolaredgeBat(AbstractBat):
40+
# Define all possible registers with their data types
41+
REGISTERS = {
42+
"Battery1StateOfEnergy": (0xe184, ModbusDataType.FLOAT_32,), # Mirror: 0xf584
43+
"Battery1InstantaneousPower": (0xe174, ModbusDataType.FLOAT_32,), # Mirror: 0xf574
44+
"Battery2StateOfEnergy": (0xe284, ModbusDataType.FLOAT_32,),
45+
"Battery2InstantaneousPower": (0xe274, ModbusDataType.FLOAT_32,),
46+
"StorageControlMode": (0xe004, ModbusDataType.UINT_16,),
47+
"StorageBackupReserved": (0xe008, ModbusDataType.FLOAT_32,),
48+
"StorageChargeDischargeDefaultMode": (0xe00a, ModbusDataType.UINT_16,),
49+
"RemoteControlCommandMode": (0xe00d, ModbusDataType.UINT_16,),
50+
"RemoteControlCommandDischargeLimit": (0xe010, ModbusDataType.FLOAT_32,),
51+
}
52+
2853
def __init__(self, component_config: SolaredgeBatSetup, **kwargs: Any) -> None:
2954
self.component_config = component_config
3055
self.kwargs: KwargsDict = kwargs
@@ -35,6 +60,9 @@ def initialize(self) -> None:
3560
self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher")
3661
self.store = get_bat_value_store(self.component_config.id)
3762
self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config))
63+
self.min_soc = 8
64+
self.StorageControlMode_Read = DEFAULT_CONTROL_MODE
65+
self.last_mode = 'undefined'
3866

3967
def update(self) -> None:
4068
self.store.set(self.read_state())
@@ -77,14 +105,167 @@ def get_values(self) -> Tuple[float, float]:
77105
power_reg, ModbusDataType.FLOAT_32, wordorder=Endian.Little, unit=unit
78106
)
79107

80-
# Handle unsupported FLOAT32 case
108+
# Handle unsupported case
81109
if power == FLOAT32_UNSUPPORTED:
82110
power = 0
111+
if soc == FLOAT32_UNSUPPORTED or not 0 <= soc <= 100:
112+
log.warning(f"Invalid SoC Speicher{battery_index}: {soc}")
113+
else:
114+
self.min_soc = min(int(soc), int(self.min_soc))
115+
log.debug(f"Min-SoC Speicher{battery_index}: {int(self.min_soc)}%.")
83116

84117
return power, soc
85118

86119
def get_imported_exported(self, power: float) -> Tuple[float, float]:
87120
return self.sim_counter.sim_count(power)
88121

122+
def set_power_limit(self, power_limit: Optional[int]) -> None:
123+
unit = self.component_config.configuration.modbus_id
124+
# Use 1 as fallback if battery_index is not set
125+
battery_index = getattr(self.component_config.configuration, "battery_index", 1)
126+
127+
try:
128+
power_limit_mode = data.data.bat_all_data.data.config.power_limit_mode
129+
except AttributeError:
130+
log.warning("power_limit_mode not found, assuming 'no_limit'")
131+
power_limit_mode = 'no_limit'
132+
133+
if power_limit_mode == 'no_limit' and self.last_mode != 'limited':
134+
"""
135+
Keine Speichersteuerung, andere Steuerungen zulassen (SolarEdge One, ioBroker, Node-Red etc.).
136+
Falls andere Steuerungen vorhanden sind, sollten diese nicht beeinflusst werden,
137+
daher erfolgt im Modus "Immer" der Speichersteuerung keine Steuerung.
138+
"""
139+
return
140+
141+
if power_limit is None:
142+
# Keine Ladung mit Speichersteuerung.
143+
if self.last_mode == 'limited':
144+
# Steuerung deaktivieren.
145+
log.debug(f"Speicher{battery_index}:Keine Steuerung gefordert, Steuerung deaktivieren.")
146+
values_to_write = {
147+
"RemoteControlCommandDischargeLimit": MAX_DISCHARGE_LIMIT,
148+
"StorageChargeDischargeDefaultMode": DEFAULT_COMMAND_MODE,
149+
"RemoteControlCommandMode": DEFAULT_COMMAND_MODE,
150+
"StorageControlMode": self.StorageControlMode_Read,
151+
}
152+
self._write_registers(values_to_write, unit)
153+
self.last_mode = None
154+
else:
155+
return
156+
157+
elif abs(power_limit) >= 0:
158+
"""
159+
Ladung mit Speichersteuerung.
160+
SolarEdge entlaedt den Speicher immer nur bis zur SoC-Reserve.
161+
Steuerung beenden, wenn der SoC vom Speicher die SoC-Reserve unterschreitet.
162+
"""
163+
registers_to_read = [
164+
f"Battery{battery_index}StateOfEnergy",
165+
"StorageControlMode",
166+
"StorageBackupReserved",
167+
"RemoteControlCommandDischargeLimit",
168+
]
169+
try:
170+
values = self._read_registers(registers_to_read, unit)
171+
except pymodbus.exceptions.ModbusException as e:
172+
log.error(f"Failed to read registers: {e}")
173+
self.fault_state.error(f"Modbus read error: {e}")
174+
return
175+
soc = values[f"Battery{battery_index}StateOfEnergy"]
176+
if soc == FLOAT32_UNSUPPORTED or not 0 <= soc <= 100:
177+
log.warning(f"Speicher{battery_index}: Invalid SoC: {soc}")
178+
soc_reserve = max(int(self.min_soc + 2), int(values["StorageBackupReserved"]))
179+
log.debug(f"SoC-Reserve Speicher{battery_index}: {int(soc_reserve)}%.")
180+
discharge_limit = int(values["RemoteControlCommandDischargeLimit"])
181+
182+
if values["StorageControlMode"] == REMOTE_CONTROL_MODE: # Speichersteuerung ist aktiv.
183+
if soc_reserve > soc:
184+
# Speichersteuerung erst deaktivieren, wenn SoC-Reserve unterschritten wird.
185+
# Darf wegen 2 Speichern nicht bereits bei SoC-Reserve deaktiviert werden!
186+
log.debug(f"Speicher{battery_index}: Steuerung deaktivieren. SoC-Reserve unterschritten")
187+
values_to_write = {
188+
"RemoteControlCommandDischargeLimit": MAX_DISCHARGE_LIMIT,
189+
"StorageChargeDischargeDefaultMode": DEFAULT_COMMAND_MODE,
190+
"RemoteControlCommandMode": DEFAULT_COMMAND_MODE,
191+
"StorageControlMode": self.StorageControlMode_Read,
192+
}
193+
self._write_registers(values_to_write, unit)
194+
self.last_mode = None
195+
196+
elif discharge_limit not in range(int(abs(power_limit)) - 10, int(abs(power_limit)) + 10):
197+
# Limit nur bei Abweichung von mehr als 10W, um Konflikte bei 2 Speichern zu verhindern.
198+
log.debug(f"Discharge-Limit Speicher{battery_index}: {int(abs(power_limit))}W.")
199+
values_to_write = {
200+
"RemoteControlCommandDischargeLimit": int(min(abs(power_limit), MAX_DISCHARGE_LIMIT))
201+
}
202+
self._write_registers(values_to_write, unit)
203+
self.last_mode = 'limited'
204+
205+
else: # Speichersteuerung ist inaktiv.
206+
if soc_reserve < soc:
207+
# Speichersteuerung nur aktivieren, wenn SoC ueber SoC-Reserve.
208+
log.debug(f"Discharge-Limit aktivieren, Speicher{battery_index}: {int(abs(power_limit))}W.")
209+
self.StorageControlMode_Read = values["StorageControlMode"]
210+
values_to_write = {
211+
"StorageControlMode": REMOTE_CONTROL_MODE,
212+
"StorageChargeDischargeDefaultMode": ACTIVE_COMMAND_MODE,
213+
"RemoteControlCommandMode": ACTIVE_COMMAND_MODE,
214+
"RemoteControlCommandDischargeLimit": int(min(abs(power_limit), MAX_DISCHARGE_LIMIT))
215+
}
216+
self._write_registers(values_to_write, unit)
217+
self.last_mode = 'limited'
218+
219+
def _read_registers(self, register_names: list, unit: int) -> Dict[str, Union[int, float]]:
220+
values = {}
221+
for key in register_names:
222+
address, data_type = self.REGISTERS[key]
223+
try:
224+
values[key] = self.__tcp_client.read_holding_registers(
225+
address, data_type, wordorder=Endian.Little, unit=unit
226+
)
227+
except pymodbus.exceptions.ModbusException as e:
228+
log.error(f"Failed to read register {key} at address {address}: {e}")
229+
self.fault_state.error(f"Modbus read error: {e}")
230+
values[key] = 0 # Fallback value
231+
log.debug(f"Bat raw values {self.__tcp_client.address}: {values}")
232+
return values
233+
# TODO: Optimize to read multiple contiguous registers in a single request if supported by ModbusTcpClient_
234+
235+
def _write_registers(self, values_to_write: Dict[str, Union[int, float]], unit: int) -> None:
236+
for key, value in values_to_write.items():
237+
address, data_type = self.REGISTERS[key]
238+
encoded_value = self._encode_value(value, data_type)
239+
try:
240+
self.__tcp_client.write_registers(address, encoded_value, unit=unit)
241+
log.debug(f"Neuer Wert {encoded_value} in Register {address} geschrieben.")
242+
except pymodbus.exceptions.ModbusException as e:
243+
log.error(f"Failed to write register {key} at address {address}: {e}")
244+
self.fault_state.error(f"Modbus write error: {e}")
245+
246+
def _encode_value(self, value: Union[int, float], data_type: ModbusDataType) -> list:
247+
builder = pymodbus.payload.BinaryPayloadBuilder(
248+
byteorder=pymodbus.constants.Endian.Big,
249+
wordorder=pymodbus.constants.Endian.Little
250+
)
251+
encode_methods = {
252+
ModbusDataType.UINT_32: builder.add_32bit_uint,
253+
ModbusDataType.INT_32: builder.add_32bit_int,
254+
ModbusDataType.UINT_16: builder.add_16bit_uint,
255+
ModbusDataType.INT_16: builder.add_16bit_int,
256+
ModbusDataType.FLOAT_32: builder.add_32bit_float,
257+
}
258+
if data_type in encode_methods:
259+
if data_type == ModbusDataType.FLOAT_32:
260+
encode_methods[data_type](float(value))
261+
else:
262+
encode_methods[data_type](int(value))
263+
else:
264+
raise ValueError(f"Unsupported data type: {data_type}")
265+
return builder.to_registers()
266+
267+
def power_limit_controllable(self) -> bool:
268+
return True
269+
89270

90271
component_descriptor = ComponentDescriptor(configuration_factory=SolaredgeBatSetup)

0 commit comments

Comments
 (0)