11#!/usr/bin/env python3
22import logging
3- from typing import Any , Tuple , TypedDict
3+
4+ from typing import Any , TypedDict , Dict , Union , Optional , Tuple
5+
46
57from pymodbus .constants import Endian
8+ import pymodbus
9+
10+
11+ from control import data
12+
613
714from modules .common import modbus
815from modules .common .abstract_device import AbstractBat
1724log = logging .getLogger (__name__ )
1825
1926FLOAT32_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
2234class KwargsDict (TypedDict ):
@@ -25,6 +37,19 @@ class KwargsDict(TypedDict):
2537
2638
2739class 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
90271component_descriptor = ComponentDescriptor (configuration_factory = SolaredgeBatSetup )
0 commit comments