Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f7cbab5
Add bat control for Sunny Island
seaspotter Apr 24, 2026
962f3af
Update bat.py
seaspotter Apr 24, 2026
f3843eb
Update bat.py
seaspotter Apr 24, 2026
bc918b2
Merge branch 'openWB:master' into feature_sma_bat
seaspotter Apr 28, 2026
3721c7f
Merge branch 'openWB:master' into feature_sma_bat
seaspotter May 6, 2026
6f98400
Merge branch 'openWB:master' into feature_sma_bat
seaspotter May 7, 2026
9fae1f8
Merge branch 'openWB:master' into feature_sma_bat
seaspotter May 11, 2026
fc6d9bd
Update SMA Sunny Island names in config.py
seaspotter May 11, 2026
a853386
Add SmaBatVersion enum for battery types
seaspotter May 11, 2026
f7d9ec2
Refactor SunnyBoyBat to include power limit control and combine bat m…
seaspotter May 11, 2026
0a91be5
Delete packages/modules/devices/sma/sma_sunny_boy/bat_tesvolt.py
seaspotter May 11, 2026
9faed24
Delete packages/modules/devices/sma/sma_sunny_boy/bat_smart_energy.py
seaspotter May 11, 2026
555324b
Refactor SMA Sunny Boy class names and parameters
seaspotter May 11, 2026
33d92ed
Refactor update method for TCP client usage
seaspotter May 11, 2026
7312db5
Update update_config.py
seaspotter May 11, 2026
e39deca
Update device.py
seaspotter May 11, 2026
f75937a
Simplify inverter.py
seaspotter May 11, 2026
b9e21ed
Update bat.py
seaspotter May 11, 2026
bb88b3b
Update update_config.py
seaspotter May 11, 2026
895f3ba
Flake8
seaspotter May 12, 2026
dee4731
Flake8
seaspotter May 12, 2026
1883987
Flake8
seaspotter May 12, 2026
3c49313
Flake8
seaspotter May 12, 2026
8f7ce29
Flake8
seaspotter May 12, 2026
8859198
Flake8
seaspotter May 12, 2026
8bc39d7
Update bat.py
seaspotter May 12, 2026
afce16f
Update bat.py
seaspotter May 12, 2026
7ad17dd
Update inverter.py
seaspotter May 12, 2026
5d7c051
Update inverter.py
seaspotter May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion packages/helpermodules/update_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@

class UpdateConfig:

DATASTORE_VERSION = 122
DATASTORE_VERSION = 123

valid_topic = [
"^openWB/bat/config/bat_control_permitted$",
Expand Down Expand Up @@ -3107,3 +3107,73 @@ def upgrade_datastore_122(self) -> None:
except Exception:
log.exception(f"Logdatei '{path}' konnte nicht konvertiert werden.")
self._append_datastore_version(122)

def upgrade_datastore_123(self) -> None:
"""
Consolidate 3 separate SMA battery modules (bat, bat_tesvolt, bat_smart_energy)
into single unified bat module with version field.
Maps old component types to new versioned structure:
- bat (with hybrid=True in inverter) -> bat with version:0 (hybrid)
- bat (with hybrid=False in inverter) -> bat with version:1 (sbs - standalone battery)
- bat_smart_energy -> bat with version:0 (hybrid)
- bat_tesvolt -> bat with version:2 (tesvolt)
"""
def upgrade_component(topic: str, payload) -> Optional[dict]:
if re.search(r"^openWB/system/device/[0-9]+/component/[0-9]+/config$", topic) is not None:
component = decode_payload(payload)
device_id = get_index(topic)
device_topic = f"openWB/system/device/{device_id}/config"
if device_topic not in self.all_received_topics:
return None

device_config = decode_payload(self.all_received_topics[device_topic])
if not str(device_config.get("type", "")).startswith("sma_sunny_boy"):
return None

if component.get("type") == "bat_tesvolt":
component["type"] = "bat"
component["configuration"] = {
"version": 2,
"modbus_id": 25
}
return {topic: component}

elif component.get("type") == "bat_smart_energy":
component["type"] = "bat"
old_config = component.get("configuration", {})
component["configuration"] = {
"version": 0,
"modbus_id": old_config.get("modbus_id", 3)
}
return {topic: component}

elif component.get("type") == "bat":
is_hybrid = False

for t, p in self.all_received_topics.items():
if re.search(f"^openWB/system/device/{device_id}/component/[0-9]+/config$", t):
comp_check = decode_payload(p)
if comp_check.get("type") == "inverter":
is_hybrid = comp_check.get("configuration", {}).get("hybrid", False)
break

old_config = component.get("configuration", {})

component["configuration"] = {
"version": 0 if is_hybrid else 1,
"modbus_id": old_config.get("modbus_id", 3)
}
return {topic: component}

return None

self._loop_all_received_topics(upgrade_component)

pub_system_message(
{},
"Die SMA Speicher-Module wurden erfolgreich zusammengeführt. "
"Deine bestehenden Einstellungen wurden automatisch übernommen.",
MessageType.INFO
)

self._append_datastore_version(123)
82 changes: 62 additions & 20 deletions packages/modules/devices/sma/sma_sunny_boy/bat.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
import logging
from typing import TypedDict, Any
from typing import Any, TypedDict, Optional

from modules.common.abstract_device import AbstractBat
from modules.common.component_state import BatState
Expand All @@ -9,8 +9,10 @@
from modules.common.modbus import ModbusTcpClient_, ModbusDataType
from modules.common.store import get_bat_value_store
from modules.devices.sma.sma_sunny_boy.config import SmaSunnyBoyBatSetup
from modules.common.simcount import SimCounter
from modules.common.utils.peak_filter import PeakFilter
from modules.common.component_type import ComponentType
from modules.devices.sma.sma_sunny_boy.bat_version import SmaBatVersion

log = logging.getLogger(__name__)

Expand All @@ -21,47 +23,87 @@ class KwargsDict(TypedDict):

class SunnyBoyBat(AbstractBat):
SMA_UINT_64_NAN = 0xFFFFFFFFFFFFFFFF # SMA uses this value to represent NaN
SMA_UINT32_NAN = 0xFFFFFFFF # SMA uses this value to represent NaN

def __init__(self, component_config: SmaSunnyBoyBatSetup, **kwargs: Any) -> None:
self.component_config = component_config
self.kwargs: KwargsDict = kwargs

def initialize(self) -> None:
self.__tcp_client: ModbusTcpClient_ = self.kwargs['client']
self.sim_counter = SimCounter(self.kwargs['device_id'], self.component_config.id, prefix="speicher")
self.store = get_bat_value_store(self.component_config.id)
Comment thread
seaspotter marked this conversation as resolved.
self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config))
self.peak_filter = PeakFilter(ComponentType.BAT, self.component_config.id, self.fault_state)
self.last_mode = 'Undefined'

def read(self) -> BatState:
def update(self) -> None:
unit = self.component_config.configuration.modbus_id

soc = self.__tcp_client.read_holding_registers(30845, ModbusDataType.UINT_32, unit=unit)
imp = self.__tcp_client.read_holding_registers(31393, ModbusDataType.INT_32, unit=unit)
exp = self.__tcp_client.read_holding_registers(31395, ModbusDataType.INT_32, unit=unit)
if imp > 5:
power = imp
else:
power = exp * -1
if self.component_config.configuration.version in (SmaBatVersion.hybrid, SmaBatVersion.sbs):
soc = self.__tcp_client.read_holding_registers(30845, ModbusDataType.UINT_32, unit=unit)
charge_power = self.__tcp_client.read_holding_registers(31393, ModbusDataType.INT_32, unit=unit)
discharge_power = self.__tcp_client.read_holding_registers(31395, ModbusDataType.INT_32, unit=unit)

if soc == self.SMA_UINT32_NAN:
# Es werden keine Werte geliefert, wenn die Battery leer ist oder nichts auf der DC Seite erzeugt wird.
soc = 0
power = 0
else:
if charge_power > 5:
power = charge_power
else:
power = discharge_power * -1

exported = self.__tcp_client.read_holding_registers(31401, ModbusDataType.UINT_64, unit=unit)
imported = self.__tcp_client.read_holding_registers(31397, ModbusDataType.UINT_64, unit=unit)

exported = self.__tcp_client.read_holding_registers(31401, ModbusDataType.UINT_64, unit=unit)
imported = self.__tcp_client.read_holding_registers(31397, ModbusDataType.UINT_64, unit=unit)
if exported == self.SMA_UINT_64_NAN or imported == self.SMA_UINT_64_NAN:
raise ValueError(f'Batterie lieferte nicht plausible Werte. Export: {exported}, Import: {imported}. ',
'Sobald die Batterie geladen/entladen wird sollte sich dieser Wert ändern, ',
'andernfalls kann ein Defekt vorliegen.')
imported, exported = self.peak_filter.check_values(power, imported, exported)

elif self.component_config.configuration.version == SmaBatVersion.tesvolt:

soc = self.__tcp_client.read_input_registers(1056, ModbusDataType.INT_32, unit=25) / 10
power = self.__tcp_client.read_input_registers(1012, ModbusDataType.INT_32, unit=25) * -1
self.peak_filter.check_values(power)
imported, exported = self.sim_counter.sim_count(power)
else:
raise ValueError('Unbekannte Batterie Version')

if exported == self.SMA_UINT_64_NAN or imported == self.SMA_UINT_64_NAN:
raise ValueError(f'Batterie lieferte nicht plausible Werte. Export: {exported}, Import: {imported}. ',
'Sobald die Batterie geladen/entladen wird sollte sich dieser Wert ändern, ',
'andernfalls kann ein Defekt vorliegen.')
imported, exported = self.peak_filter.check_values(power, imported, exported)
bat_state = BatState(
power=power,
soc=soc,
imported=imported,
exported=exported
)
log.debug("Bat {}: {}".format(self.__tcp_client.address, bat_state))
return bat_state
self.store.set(bat_state)

def update(self) -> None:
self.store.set(self.read())
def set_power_limit(self, power_limit: Optional[int]) -> None:
unit = self.component_config.configuration.modbus_id

if power_limit is None:
if self.last_mode is not None:
# Kein Powerlimit gefordert, externe Steuerung war aktiv, externe Steuerung deaktivieren
self.__tcp_client.write_register(40151, 803, data_type=ModbusDataType.UINT_32, unit=unit)
self.__tcp_client.write_register(40149, 0, data_type=ModbusDataType.INT_32, unit=unit)
log.debug("Keine Batteriesteuerung gefordert, deaktiviere externe Steuerung.")
self.last_mode = None
else:
# Powerlimit gefordert, externe Steuerung aktivieren, Limit setzen
self.__tcp_client.write_register(40151, 802, data_type=ModbusDataType.UINT_32, unit=unit)
power_value = int(power_limit) * -1
self.__tcp_client.write_register(40149, power_value, data_type=ModbusDataType.INT_32, unit=unit)
log.debug(f"Aktive Batteriesteuerung vorhanden. Setze externe Steuerung. Leistung: {power_value}")
self.last_mode = 'limited'

def power_limit_controllable(self) -> bool:
return self.component_config.configuration.version in (
SmaBatVersion.hybrid,
SmaBatVersion.sbs
)


component_descriptor = ComponentDescriptor(configuration_factory=SmaSunnyBoyBatSetup)
145 changes: 0 additions & 145 deletions packages/modules/devices/sma/sma_sunny_boy/bat_smart_energy.py

This file was deleted.

Loading
Loading