11#!/usr/bin/env python3
2+ from enum import IntEnum
23import logging
3-
44from typing import Any , TypedDict , Dict , Union , Optional , Tuple
5-
6-
75from pymodbus .constants import Endian
8- import pymodbus
9-
106
117from modules .common import modbus
128from modules .common .abstract_device import AbstractBat
2016
2117log = logging .getLogger (__name__ )
2218
23- FLOAT32_UNSUPPORTED = - 0xffffff00000000000000000000000000
19+ FLOAT32_UNSUPPORTED = - 0xFFFFFF00
2420MAX_CHARGEDISCHARGE_LIMIT = 5000
21+ DEFAULT_SOC = 50.0 # Fallback bei ungültigem SoC
2522CONTROL_MODE_MSC = 1 # Storage Control Mode Maximize Self Consumption
2623CONTROL_MODE_REMOTE = 4 # Control Mode Remotesteuerung
2724REMOTE_CONTROL_COMMAND_MODE_DEFAULT = 0 # Default RC Command Mode ohne Steuerung
@@ -34,21 +31,28 @@ class KwargsDict(TypedDict):
3431 client : modbus .ModbusTcpClient_
3532
3633
37- class SolaredgeBat (AbstractBat ):
38- # Define all possible registers with their data types
39- REGISTERS = {
40- "Battery1StateOfEnergy" : (0xe184 , ModbusDataType .FLOAT_32 ,), # Mirror: 0xf584
41- "Battery1InstantaneousPower" : (0xe174 , ModbusDataType .FLOAT_32 ,), # Mirror: 0xf574
42- "Battery2StateOfEnergy" : (0xe284 , ModbusDataType .FLOAT_32 ,),
43- "Battery2InstantaneousPower" : (0xe274 , ModbusDataType .FLOAT_32 ,),
44- "StorageControlMode" : (0xe004 , ModbusDataType .UINT_16 ,),
45- "StorageBackupReserved" : (0xe008 , ModbusDataType .FLOAT_32 ,),
46- "RemoteControlCommandModeDefault" : (0xe00a , ModbusDataType .UINT_16 ,),
47- "RemoteControlCommandMode" : (0xe00d , ModbusDataType .UINT_16 ,),
48- "RemoteControlChargeLimit" : (0xe00e , ModbusDataType .FLOAT_32 ,),
49- "RemoteControlDischargeLimit" : (0xe010 , ModbusDataType .FLOAT_32 ,),
50- }
34+ class Registers (IntEnum ):
35+ STORAGE_CONTROL_MODE = 0xe004
36+ REMOTE_CONTROL_COMMAND_MODE_DEFAULT_REG = 0xe00a
37+ REMOTE_CONTROL_COMMAND_MODE = 0xe00d
38+ REMOTE_CONTROL_CHARGE_LIMIT = 0xe00e
39+ REMOTE_CONTROL_DISCHARGE_LIMIT = 0xe010
40+ BAT_1_POWER = 0xe174
41+ BAT_1_SOC = 0xe184
42+ BAT_2_POWER = 0xe274
43+ BAT_2_SOC = 0xe284
44+
45+
46+ WRITING_DATA_TYPES = {
47+ Registers .STORAGE_CONTROL_MODE : ModbusDataType .UINT_16 ,
48+ Registers .REMOTE_CONTROL_COMMAND_MODE_DEFAULT_REG : ModbusDataType .UINT_16 ,
49+ Registers .REMOTE_CONTROL_COMMAND_MODE : ModbusDataType .UINT_16 ,
50+ Registers .REMOTE_CONTROL_CHARGE_LIMIT : ModbusDataType .FLOAT_32 ,
51+ Registers .REMOTE_CONTROL_DISCHARGE_LIMIT : ModbusDataType .FLOAT_32 ,
52+ }
53+
5154
55+ class SolaredgeBat (AbstractBat ):
5256 def __init__ (self , component_config : SolaredgeBatSetup , ** kwargs : Any ) -> None :
5357 self .component_config = component_config
5458 self .kwargs : KwargsDict = kwargs
@@ -75,37 +79,25 @@ def read_state(self):
7579
7680 def get_values (self ) -> Tuple [float , float ]:
7781 unit = self .component_config .configuration .modbus_id
78- # Use 1 as fallback if battery_index is not set
7982 battery_index = getattr (self .component_config .configuration , "battery_index" , 1 )
80-
81- # Define base registers for Battery 1 in hex
82- base_soc_reg = 0xE184 # Battery 1 SoC
83- base_power_reg = 0xE174 # Battery 1 Power
84- offset = 0x100 # 256 bytes in hex
85-
86- # Adjust registers based on battery_index
87- if battery_index == 1 :
88- soc_reg = base_soc_reg
89- power_reg = base_power_reg
90- elif battery_index == 2 :
91- soc_reg = base_soc_reg + offset # 0xE284
92- power_reg = base_power_reg + offset # 0xE274
93- else :
94- raise ValueError (f"Invalid battery_index: { battery_index } . Must be 1 or 2." )
95-
96- # Read SoC and Power from the appropriate registers
97- soc = self .__tcp_client .read_holding_registers (
98- soc_reg , ModbusDataType .FLOAT_32 , wordorder = Endian .Little , unit = unit
99- )
100- power = self .__tcp_client .read_holding_registers (
101- power_reg , ModbusDataType .FLOAT_32 , wordorder = Endian .Little , unit = unit
83+ power_reg = Registers .BAT_1_POWER if battery_index == 1 else Registers .BAT_2_POWER
84+ soc_reg = Registers .BAT_1_SOC if battery_index == 1 else Registers .BAT_2_SOC
85+ bulk = (
86+ (power_reg , ModbusDataType .FLOAT_32 ),
87+ (soc_reg , ModbusDataType .FLOAT_32 ),
10288 )
10389
90+ resp = self .__tcp_client .read_holding_registers_bulk (
91+ power_reg , 18 , mapping = bulk , wordorder = Endian .Little , unit = unit )
92+ log .debug (f"Bat raw values { self .__tcp_client .address } : { resp } " )
93+ power = resp [power_reg ]
94+ soc = resp [soc_reg ]
10495 # Handle unsupported case
10596 if power == FLOAT32_UNSUPPORTED :
10697 power = 0
10798 if soc == FLOAT32_UNSUPPORTED or not 0 <= soc <= 100 :
108- log .warning (f"Invalid SoC Speicher{ battery_index } : { soc } " )
99+ log .warning (f"Invalid SoC Speicher{ battery_index } : { soc } , using default" )
100+ soc = DEFAULT_SOC
109101
110102 return power , soc
111103
@@ -114,110 +106,88 @@ def get_imported_exported(self, power: float) -> Tuple[float, float]:
114106
115107 def set_power_limit (self , power_limit : Optional [int ]) -> None :
116108 unit = self .component_config .configuration .modbus_id
117- # Use 1 as fallback if battery_index is not set
118109 battery_index = getattr (self .component_config .configuration , "battery_index" , 1 )
119110
120- registers_to_read = [
121- "StorageControlMode" ,
122- "RemoteControlCommandMode" ,
123- "RemoteControlChargeLimit" ,
124- "RemoteControlDischargeLimit" ,
125- ]
126- try :
127- values = self ._read_registers (registers_to_read , unit )
128- except pymodbus .exceptions .ModbusException as e :
129- log .error (f"Failed to read registers: { e } " )
130- self .fault_state .error (f"Modbus read error: { e } " )
131- return
111+ bulk = (
112+ (Registers .STORAGE_CONTROL_MODE , ModbusDataType .UINT_16 ),
113+ (Registers .REMOTE_CONTROL_COMMAND_MODE_DEFAULT_REG , ModbusDataType .UINT_16 ),
114+ (Registers .REMOTE_CONTROL_COMMAND_MODE , ModbusDataType .UINT_16 ),
115+ (Registers .REMOTE_CONTROL_CHARGE_LIMIT , ModbusDataType .FLOAT_32 ),
116+ (Registers .REMOTE_CONTROL_DISCHARGE_LIMIT , ModbusDataType .FLOAT_32 ),
117+ )
118+
119+ values = self .__tcp_client .read_holding_registers_bulk (
120+ Registers .STORAGE_CONTROL_MODE , 13 , mapping = bulk , unit = unit )
121+ log .debug (f"Bat raw values { self .__tcp_client .address } : { values } " )
132122
133123 if power_limit is None : # No Bat Control should be used.
134- if values ["StorageControlMode" ] == CONTROL_MODE_MSC :
124+ if values [Registers . STORAGE_CONTROL_MODE ] == CONTROL_MODE_MSC :
135125 log .debug (f"Speicher{ battery_index } :Keine Steuerung gefordert, bereits deaktiviert." )
136126 else :
137127 # Disable Bat Control
138128 values_to_write = {
139- "RemoteControlChargeLimit" : MAX_CHARGEDISCHARGE_LIMIT ,
140- "RemoteControlDischargeLimit" : MAX_CHARGEDISCHARGE_LIMIT ,
141- "RemoteControlCommandModeDefault" : REMOTE_CONTROL_COMMAND_MODE_DEFAULT ,
142- "RemoteControlCommandMode" : REMOTE_CONTROL_COMMAND_MODE_DEFAULT ,
143- "StorageControlMode" : CONTROL_MODE_MSC ,
129+ Registers . REMOTE_CONTROL_CHARGE_LIMIT : MAX_CHARGEDISCHARGE_LIMIT ,
130+ Registers . REMOTE_CONTROL_DISCHARGE_LIMIT : MAX_CHARGEDISCHARGE_LIMIT ,
131+ Registers . REMOTE_CONTROL_COMMAND_MODE_DEFAULT_REG : REMOTE_CONTROL_COMMAND_MODE_DEFAULT ,
132+ Registers . REMOTE_CONTROL_COMMAND_MODE : REMOTE_CONTROL_COMMAND_MODE_DEFAULT ,
133+ Registers . STORAGE_CONTROL_MODE : CONTROL_MODE_MSC ,
144134 }
145135 self ._write_registers (values_to_write , unit )
146136 log .debug (f"Speicher{ battery_index } :Keine Steuerung gefordert, Steuerung deaktiviert." )
147137
148138 elif power_limit <= 0 : # Limit Discharge Mode should be used.
149- if (values ["StorageControlMode" ] == CONTROL_MODE_REMOTE and
150- values ["RemoteControlCommandMode" ] == REMOTE_CONTROL_COMMAND_MODE_MSC ):
139+ if (values [Registers . STORAGE_CONTROL_MODE ] == CONTROL_MODE_REMOTE and
140+ values [Registers . REMOTE_CONTROL_COMMAND_MODE ] == REMOTE_CONTROL_COMMAND_MODE_MSC ):
151141 # Remote Control and Discharge Mode already active.
152- discharge_limit = int (values ["RemoteControlDischargeLimit" ])
142+ discharge_limit = int (values [Registers . REMOTE_CONTROL_DISCHARGE_LIMIT ])
153143 if discharge_limit not in range (int (abs (power_limit )) - 10 , int (abs (power_limit )) + 10 ):
154144 # Send Limit only if difference is more than 10W, needed with more than 1 battery.
155145 values_to_write = {
156- "RemoteControlDischargeLimit" : int (min (abs (power_limit ), MAX_CHARGEDISCHARGE_LIMIT ))
146+ Registers . REMOTE_CONTROL_DISCHARGE_LIMIT : int (min (abs (power_limit ), MAX_CHARGEDISCHARGE_LIMIT ))
157147 }
158148 self ._write_registers (values_to_write , unit )
159149 log .debug (f"Entlade-Limit Speicher{ battery_index } : { int (abs (power_limit ))} W." )
160150 else :
161151 log .debug (f"Entlade-Limit Speicher{ battery_index } : Abweichung unter +/- 10W." )
162152 else : # Enable Remote Control and Discharge Mode.
163153 values_to_write = {
164- "StorageControlMode" : CONTROL_MODE_REMOTE ,
165- "RemoteControlCommandModeDefault" : REMOTE_CONTROL_COMMAND_MODE_MSC ,
166- "RemoteControlCommandMode" : REMOTE_CONTROL_COMMAND_MODE_MSC ,
167- "RemoteControlDischargeLimit" : int (min (abs (power_limit ), MAX_CHARGEDISCHARGE_LIMIT ))
154+ Registers . STORAGE_CONTROL_MODE : CONTROL_MODE_REMOTE ,
155+ Registers . REMOTE_CONTROL_COMMAND_MODE_DEFAULT_REG : REMOTE_CONTROL_COMMAND_MODE_MSC ,
156+ Registers . REMOTE_CONTROL_COMMAND_MODE : REMOTE_CONTROL_COMMAND_MODE_MSC ,
157+ Registers . REMOTE_CONTROL_DISCHARGE_LIMIT : int (min (abs (power_limit ), MAX_CHARGEDISCHARGE_LIMIT ))
168158 }
169159 self ._write_registers (values_to_write , unit )
170160 log .debug (f"Entlade-Limit aktiviert, Speicher{ battery_index } : { int (abs (power_limit ))} W." )
171161
172162 elif power_limit > 0 : # Charge Mode should be used
173- if (values ["StorageControlMode" ] == CONTROL_MODE_REMOTE and
174- values ["RemoteControlCommandMode" ] == REMOTE_CONTROL_COMMAND_MODE_CHARGE ):
163+ if (values [Registers . STORAGE_CONTROL_MODE ] == CONTROL_MODE_REMOTE and
164+ values [Registers . REMOTE_CONTROL_COMMAND_MODE ] == REMOTE_CONTROL_COMMAND_MODE_CHARGE ):
175165 # Remote Control and Charge Mode already active.
176- charge_limit = int (values ["RemoteControlChargeLimit" ])
166+ charge_limit = int (values [Registers . REMOTE_CONTROL_CHARGE_LIMIT ])
177167 if charge_limit not in range (int (abs (power_limit )) - 10 , int (abs (power_limit )) + 10 ):
178168 # Send Limit only if difference is more than 10W.
179169 values_to_write = {
180- "RemoteControlChargeLimit" : int (min (abs (power_limit ), MAX_CHARGEDISCHARGE_LIMIT ))
170+ Registers . REMOTE_CONTROL_CHARGE_LIMIT : int (min (abs (power_limit ), MAX_CHARGEDISCHARGE_LIMIT ))
181171 }
182172 self ._write_registers (values_to_write , unit )
183173 log .debug (f"Ladung Speicher{ battery_index } : { int (abs (power_limit ))} W." )
184174 else :
185175 log .debug (f"Ladung Speicher{ battery_index } : Abweichung unter +/- 10W." )
186176 else : # Enable Remote Control and Charge Mode.
187177 values_to_write = {
188- "StorageControlMode" : CONTROL_MODE_REMOTE ,
189- "RemoteControlCommandModeDefault" : REMOTE_CONTROL_COMMAND_MODE_CHARGE ,
190- "RemoteControlCommandMode" : REMOTE_CONTROL_COMMAND_MODE_CHARGE ,
191- "RemoteControlChargeLimit" : int (min (abs (power_limit ), MAX_CHARGEDISCHARGE_LIMIT ))
178+ Registers . STORAGE_CONTROL_MODE : CONTROL_MODE_REMOTE ,
179+ Registers . REMOTE_CONTROL_COMMAND_MODE_DEFAULT_REG : REMOTE_CONTROL_COMMAND_MODE_CHARGE ,
180+ Registers . REMOTE_CONTROL_COMMAND_MODE : REMOTE_CONTROL_COMMAND_MODE_CHARGE ,
181+ Registers . REMOTE_CONTROL_CHARGE_LIMIT : int (min (abs (power_limit ), MAX_CHARGEDISCHARGE_LIMIT ))
192182 }
193183 self ._write_registers (values_to_write , unit )
194184 log .debug (f"Aktivierung Ladung Speicher{ battery_index } : { int (abs (power_limit ))} W." )
195185
196- def _read_registers (self , register_names : list , unit : int ) -> Dict [str , Union [int , float ]]:
197- values = {}
198- for key in register_names :
199- address , data_type = self .REGISTERS [key ]
200- try :
201- values [key ] = self .__tcp_client .read_holding_registers (
202- address , data_type , wordorder = Endian .Little , unit = unit
203- )
204- except pymodbus .exceptions .ModbusException as e :
205- log .error (f"Failed to read register { key } at address { address } : { e } " )
206- self .fault_state .error (f"Modbus read error: { e } " )
207- values [key ] = 0 # Fallback value
208- log .debug (f"Bat raw values { self .__tcp_client .address } : { values } " )
209- return values
210- # TODO: Optimize to read multiple contiguous registers in a single request if supported by ModbusTcpClient_
211-
212- def _write_registers (self , values_to_write : Dict [str , Union [int , float ]], unit : int ) -> None :
213- for key , value in values_to_write .items ():
214- address , data_type = self .REGISTERS [key ]
215- try :
216- self .__tcp_client .write_register (address , value , data_type , wordorder = Endian .Little , unit = unit )
217- log .debug (f"Neuer Wert { value } in Register { address } geschrieben." )
218- except pymodbus .exceptions .ModbusException as e :
219- log .error (f"Failed to write register { key } at address { address } : { e } " )
220- self .fault_state .error (f"Modbus write error: { e } " )
186+ def _write_registers (self , values_to_write : Dict [Registers , Union [int , float ]], unit : int ) -> None :
187+ for address , value in values_to_write .items ():
188+ self .__tcp_client .write_register (
189+ address , value , WRITING_DATA_TYPES [address ], wordorder = Endian .Little , unit = unit )
190+ log .debug (f"Neuer Wert { value } in Register { address } geschrieben." )
221191
222192 def power_limit_controllable (self ) -> bool :
223193 return True
0 commit comments