Skip to content

Commit 9b2e7c6

Browse files
committed
solaredge: use modbus bulk reader for bat
1 parent f7e4e31 commit 9b2e7c6

1 file changed

Lines changed: 74 additions & 104 deletions

File tree

  • packages/modules/devices/solaredge/solaredge

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

Lines changed: 74 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
#!/usr/bin/env python3
2+
from enum import IntEnum
23
import logging
3-
44
from typing import Any, TypedDict, Dict, Union, Optional, Tuple
5-
6-
75
from pymodbus.constants import Endian
8-
import pymodbus
9-
106

117
from modules.common import modbus
128
from modules.common.abstract_device import AbstractBat
@@ -20,8 +16,9 @@
2016

2117
log = logging.getLogger(__name__)
2218

23-
FLOAT32_UNSUPPORTED = -0xffffff00000000000000000000000000
19+
FLOAT32_UNSUPPORTED = -0xFFFFFF00
2420
MAX_CHARGEDISCHARGE_LIMIT = 5000
21+
DEFAULT_SOC = 50.0 # Fallback bei ungültigem SoC
2522
CONTROL_MODE_MSC = 1 # Storage Control Mode Maximize Self Consumption
2623
CONTROL_MODE_REMOTE = 4 # Control Mode Remotesteuerung
2724
REMOTE_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

Comments
 (0)