Skip to content

Commit 7aba53c

Browse files
authored
SMA Speichersteuerung (#2228)
* read bat power from modbus, remove calculation from voltage and current * improve logging * refactor read modbus values, add logging * fix typo * trigger build * refactor SMA Sunny Boy battery module: consolidate register definitions and add power limit functionality * fix attribute error * refactor for readability, skip unecessary modbus writes when there is no change * fix pytest * refactor SunnyBoySmartEnergyBat: optimize register reading and improve logging * fix logic for last_mode * fix inverter_type * remove debug loggig * fix inverter_type
1 parent 6001d71 commit 7aba53c

3 files changed

Lines changed: 122 additions & 19 deletions

File tree

packages/modules/devices/sma/sma_sunny_boy/bat.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python3
2+
import logging
23
from typing import Dict, Union
34

45
from dataclass_utils import dataclass_from_dict
@@ -10,6 +11,8 @@
1011
from modules.common.store import get_bat_value_store
1112
from modules.devices.sma.sma_sunny_boy.config import SmaSunnyBoyBatSetup
1213

14+
log = logging.getLogger(__name__)
15+
1316

1417
class SunnyBoyBat(AbstractBat):
1518
SMA_UINT_64_NAN = 0xFFFFFFFFFFFFFFFF # SMA uses this value to represent NaN
@@ -42,12 +45,14 @@ def read(self) -> BatState:
4245
'Sobald die Batterie geladen/entladen wird sollte sich dieser Wert ändern, ',
4346
'andernfalls kann ein Defekt vorliegen.')
4447

45-
return BatState(
48+
bat_state = BatState(
4649
power=power,
4750
soc=soc,
4851
imported=imported,
4952
exported=exported
5053
)
54+
log.debug("Bat {}: {}".format(self.__tcp_client.address, bat_state))
55+
return bat_state
5156

5257
def update(self) -> None:
5358
self.store.set(self.read())
Lines changed: 112 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env python3
2-
from typing import Dict, Union
2+
import logging
3+
from typing import Dict, Union, Optional
34

45
from dataclass_utils import dataclass_from_dict
56
from modules.common.abstract_device import AbstractBat
@@ -10,12 +11,28 @@
1011
from modules.common.simcount import SimCounter
1112
from modules.common.store import get_bat_value_store
1213
from modules.devices.sma.sma_sunny_boy.config import SmaSunnyBoySmartEnergyBatSetup
14+
import pymodbus
15+
16+
17+
log = logging.getLogger(__name__)
1318

1419

1520
class SunnyBoySmartEnergyBat(AbstractBat):
1621
SMA_UINT32_NAN = 0xFFFFFFFF # SMA uses this value to represent NaN
1722
SMA_UINT_64_NAN = 0xFFFFFFFFFFFFFFFF # SMA uses this value to represent NaN
1823

24+
# Define all possible registers with their data types
25+
REGISTERS = {
26+
"Battery_SoC": (30845, ModbusDataType.UINT_32),
27+
"Battery_ChargePower": (31393, ModbusDataType.INT_32),
28+
"Battery_DischargePower": (31395, ModbusDataType.INT_32),
29+
"Battery_ChargedEnergy": (31397, ModbusDataType.UINT_64),
30+
"Battery_DischargedEnergy": (31401, ModbusDataType.UINT_64),
31+
"Inverter_Type": (30053, ModbusDataType.UINT_32),
32+
"Externe_Steuerung": (40151, ModbusDataType.UINT_32),
33+
"Wirkleistungsvorgabe": (40149, ModbusDataType.UINT_32),
34+
}
35+
1936
def __init__(self,
2037
device_id: int,
2138
component_config: Union[Dict, SmaSunnyBoySmartEnergyBatSetup],
@@ -26,37 +43,115 @@ def __init__(self,
2643
self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher")
2744
self.store = get_bat_value_store(self.component_config.id)
2845
self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config))
46+
self.last_mode = 'Undefined'
47+
self.inverter_type = None
2948

3049
def update(self) -> None:
3150
self.store.set(self.read())
3251

3352
def read(self) -> BatState:
3453
unit = self.component_config.configuration.modbus_id
3554

36-
soc = self.__tcp_client.read_holding_registers(30845, ModbusDataType.UINT_32, unit=unit)
37-
current = self.__tcp_client.read_holding_registers(30843, ModbusDataType.INT_32, unit=unit)/-1000
38-
voltage = self.__tcp_client.read_holding_registers(30851, ModbusDataType.INT_32, unit=unit)/100
55+
registers_to_read = [
56+
"Battery_SoC",
57+
"Battery_ChargePower",
58+
"Battery_DischargePower",
59+
"Battery_ChargedEnergy",
60+
"Battery_DischargedEnergy"
61+
]
62+
63+
if self.inverter_type is None: # Only read Inverter_Type if not already set
64+
registers_to_read.append("Inverter_Type")
3965

40-
if soc == self.SMA_UINT32_NAN:
66+
values = self._read_registers(registers_to_read, unit)
67+
68+
if values["Battery_SoC"] == self.SMA_UINT32_NAN:
4169
# If the storage is empty and nothing is produced on the DC side, the inverter does not supply any values.
42-
soc = 0
70+
values["Battery_SoC"] = 0
4371
power = 0
4472
else:
45-
power = current*voltage
46-
exported = self.__tcp_client.read_holding_registers(31401, ModbusDataType.UINT_64, unit=3)
47-
imported = self.__tcp_client.read_holding_registers(31397, ModbusDataType.UINT_64, unit=3)
73+
if values["Battery_ChargePower"] > 5:
74+
power = values["Battery_ChargePower"]
75+
else:
76+
power = values["Battery_DischargePower"] * -1
4877

49-
if exported == self.SMA_UINT_64_NAN or imported == self.SMA_UINT_64_NAN:
50-
raise ValueError(f'Batterie lieferte nicht plausible Werte. Export: {exported}, Import: {imported}. ',
51-
'Sobald die Batterie geladen/entladen wird sollte sich dieser Wert ändern, ',
52-
'andernfalls kann ein Defekt vorliegen.')
78+
if (values["Battery_ChargedEnergy"] == self.SMA_UINT_64_NAN or
79+
values["Battery_DischargedEnergy"] == self.SMA_UINT_64_NAN):
80+
raise ValueError(
81+
f'Batterie lieferte nicht plausible Werte. Geladene Energie: {values["Battery_ChargedEnergy"]}, '
82+
f'Entladene Energie: {values["Battery_DischargedEnergy"]}. ',
83+
'Sobald die Batterie geladen/entladen wird sollte sich dieser Wert ändern, ',
84+
'andernfalls kann ein Defekt vorliegen.'
85+
)
5386

54-
return BatState(
87+
bat_state = BatState(
5588
power=power,
56-
soc=soc,
57-
imported=imported,
58-
exported=exported
89+
soc=values["Battery_SoC"],
90+
exported=values["Battery_DischargedEnergy"],
91+
imported=values["Battery_ChargedEnergy"]
5992
)
93+
if self.inverter_type is None:
94+
self.inverter_type = values["Inverter_Type"]
95+
log.debug(f"Inverter Type: {self.inverter_type}")
96+
log.debug(f"Bat {self.__tcp_client.address}: {bat_state}")
97+
return bat_state
98+
99+
def set_power_limit(self, power_limit: Optional[int]) -> None:
100+
unit = self.component_config.configuration.modbus_id
101+
102+
if power_limit is None:
103+
if self.last_mode is not None:
104+
# Kein Powerlimit gefordert, externe Steuerung war aktiv, externe Steuerung deaktivieren
105+
log.debug("Keine Batteriesteuerung gefordert, deaktiviere externe Steuerung.")
106+
values_to_write = {
107+
"Externe_Steuerung": 803,
108+
"Wirkleistungsvorgabe": 0,
109+
}
110+
self._write_registers(values_to_write, unit)
111+
self.last_mode = None
112+
else:
113+
# Powerlimit gefordert, externe Steuerung aktivieren, Limit setzen
114+
log.debug("Aktive Batteriesteuerung vorhanden. Setze externe Steuerung.")
115+
values_to_write = {
116+
"Externe_Steuerung": 802,
117+
"Wirkleistungsvorgabe": power_limit
118+
}
119+
self._write_registers(values_to_write, unit)
120+
self.last_mode = 'limited'
121+
122+
def _read_registers(self, register_names: list, unit: int) -> Dict[str, Union[int, float]]:
123+
values = {}
124+
for key in register_names:
125+
address, data_type = self.REGISTERS[key]
126+
values[key] = self.__tcp_client.read_holding_registers(address, data_type, unit=unit)
127+
log.debug(f"Bat raw values {self.__tcp_client.address}: {values}")
128+
return values
129+
130+
def _write_registers(self, values_to_write: Dict[str, Union[int, float]], unit: int) -> None:
131+
for key, value in values_to_write.items():
132+
address, data_type = self.REGISTERS[key]
133+
encoded_value = self._encode_value(value, data_type)
134+
self.__tcp_client.write_registers(address, encoded_value, unit=unit)
135+
log.debug(f"Neuer Wert {encoded_value} in Register {address} geschrieben.")
136+
137+
def _encode_value(self, value: Union[int, float], data_type: ModbusDataType) -> list:
138+
builder = pymodbus.payload.BinaryPayloadBuilder(
139+
byteorder=pymodbus.constants.Endian.Big,
140+
wordorder=pymodbus.constants.Endian.Big
141+
)
142+
encode_methods = {
143+
ModbusDataType.UINT_32: builder.add_32bit_uint,
144+
ModbusDataType.INT_32: builder.add_32bit_int,
145+
ModbusDataType.UINT_16: builder.add_16bit_uint,
146+
ModbusDataType.INT_16: builder.add_16bit_int,
147+
}
148+
149+
if data_type in encode_methods:
150+
encode_methods[data_type](int(value))
151+
else:
152+
raise ValueError(f"Unsupported data type: {data_type}")
153+
154+
return builder.to_registers()
60155

61156

62157
component_descriptor = ComponentDescriptor(configuration_factory=SmaSunnyBoySmartEnergyBatSetup)

packages/modules/devices/sma/sma_sunny_boy/bat_tesvolt.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python3
2+
import logging
23

34
from modules.common.abstract_device import AbstractBat
45
from modules.common.component_state import BatState
@@ -9,6 +10,8 @@
910
from modules.common.store import get_bat_value_store
1011
from modules.devices.sma.sma_sunny_boy.config import SmaTesvoltBatSetup
1112

13+
log = logging.getLogger(__name__)
14+
1215

1316
class TesvoltBat(AbstractBat):
1417
def __init__(self,
@@ -32,7 +35,7 @@ def update(self) -> None:
3235
imported=imported,
3336
exported=exported
3437
)
35-
38+
log.debug("Bat {}: {}".format(self.__tcp_client.address, bat_state))
3639
self.store.set(bat_state)
3740

3841

0 commit comments

Comments
 (0)