Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 18 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 = 118
DATASTORE_VERSION = 119

valid_topic = [
"^openWB/bat/config/bat_control_permitted$",
Expand Down Expand Up @@ -3028,3 +3028,20 @@ def upgrade(topic: str, payload) -> Optional[dict]:
return {topic: configuration_payload}
self._loop_all_received_topics(upgrade)
self._append_datastore_version(118)

def upgrade_datastore_119(self) -> None:
def upgrade(topic: str, payload) -> None:
if re.search("openWB/system/device/[0-9]+", topic) is not None:
payload = decode_payload(payload)
index = get_index(topic)
if payload.get("type") == "victron":
for component_topic, component_payload in self.all_received_topics.items():
if re.search(f"openWB/system/device/{index}/component/[0-9]+/config$",
component_topic) is not None:
config_payload = decode_payload(component_payload)
if (config_payload["type"] == "bat" and
config_payload["configuration"].get("vebus_id") is not None):
config_payload["configuration"] = config_payload["configuration"].pop("vebus_id")
return {topic: config_payload}
Comment on lines +3044 to +3045

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In upgrade_datastore_119, the removal of vebus_id is implemented incorrectly: config_payload["configuration"] = config_payload["configuration"].pop("vebus_id") replaces the whole configuration dict with the popped scalar value. Additionally, the returned topic key uses topic (device topic) instead of component_topic, so the modified component config would be published to the wrong MQTT topic. Use config_payload["configuration"].pop("vebus_id", None) and return {component_topic: config_payload} (keeping configuration as a dict).

Suggested change
config_payload["configuration"] = config_payload["configuration"].pop("vebus_id")
return {topic: config_payload}
config_payload["configuration"].pop("vebus_id", None)
return {component_topic: config_payload}

Copilot uses AI. Check for mistakes.
Comment on lines +3033 to +3045

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upgrade_datastore_119 introduces new datastore-migration behavior (removal of vebus_id from Victron battery component configuration), but there is no corresponding unit test added. helpermodules/update_config_test.py already covers other upgrade_datastore_* methods, so adding a test case for version 119 would help prevent regressions in future migrations.

Suggested change
def upgrade(topic: str, payload) -> None:
if re.search("openWB/system/device/[0-9]+", topic) is not None:
payload = decode_payload(payload)
index = get_index(topic)
if payload.get("type") == "victron":
for component_topic, component_payload in self.all_received_topics.items():
if re.search(f"openWB/system/device/{index}/component/[0-9]+/config$",
component_topic) is not None:
config_payload = decode_payload(component_payload)
if (config_payload["type"] == "bat" and
config_payload["configuration"].get("vebus_id") is not None):
config_payload["configuration"] = config_payload["configuration"].pop("vebus_id")
return {topic: config_payload}
def upgrade(topic: str, payload) -> Optional[dict]:
# Remove deprecated `vebus_id` from Victron battery component configurations.
# Operate directly on component config topics to avoid iterating inside the callback.
if re.search(r"^openWB/system/device/[0-9]+/component/[0-9]+/config$", topic) is None:
return None
config_payload = decode_payload(payload)
if config_payload.get("type") != "bat":
return None
configuration = config_payload.get("configuration", {})
if "vebus_id" not in configuration:
return None
# Drop the obsolete field while preserving the rest of the configuration.
configuration.pop("vebus_id", None)
return {topic: config_payload}

Copilot uses AI. Check for mistakes.
self._loop_all_received_topics(upgrade)
self._append_datastore_version(119)
75 changes: 33 additions & 42 deletions packages/modules/devices/victron/victron/bat.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from modules.devices.victron.victron.config import VictronBatSetup
from modules.common.utils.peak_filter import PeakFilter
from modules.common.component_type import ComponentType
from control import data

log = logging.getLogger(__name__)

Expand All @@ -35,6 +36,7 @@ def initialize(self) -> None:
self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config))
self.last_mode = 'Undefined'
self.peak_filter = PeakFilter(ComponentType.BAT, self.component_config.id, self.fault_state)
self.current_power = 0

def update(self) -> None:
modbus_id = self.component_config.configuration.modbus_id
Expand All @@ -50,73 +52,62 @@ def update(self) -> None:
exported=exported
)
self.store.set(bat_state)
self.current_power = power

def set_power_limit(self, power_limit: Optional[int]) -> None:
modbus_id = self.component_config.configuration.modbus_id
vebus_id = self.component_config.configuration.vebus_id
# Wenn Victron Dynamic ESS aktiv, erfolgt keine weitere Regelung in openWB
dynamic_ess_mode = self.__tcp_client.read_holding_registers(5400, ModbusDataType.UINT_16, unit=modbus_id)
if dynamic_ess_mode == 1:
log.debug("Dynamic ESS Mode ist aktiv, daher erfolgt keine Regelung des Speichers durch openWB")
return

phases = self.__tcp_client.read_holding_registers(28, ModbusDataType.UINT_16, unit=vebus_id)
if phases == 1:
log.debug("Einphasiger Speicher erkannt, Speichersteuerung nur auf der ersten Phase.")
else:
log.debug("Mehrphasiger Speicher erkannt, Speichersteuerung auf 3 Phasen.")
if power_limit is None:
log.debug("Keine Batteriesteuerung, Selbstregelung durch Wechselrichter")
if self.last_mode is not None:
# ESS Mode 1 für Selbstregelung mit Phasenkompensation setzen
self.__tcp_client.write_register(2902, 1, data_type=ModbusDataType.UINT_16, unit=modbus_id)
self.__tcp_client.write_register(39, 0, data_type=ModbusDataType.UINT_16, unit=vebus_id)
# ESS Mode 2 und Leistung EVU auf 0kW setzen für Selbstregelung
self.__tcp_client.write_register(2902, 2, data_type=ModbusDataType.UINT_16, unit=modbus_id)
self.__tcp_client.write_register(2702, 100, data_type=ModbusDataType.UINT_16, unit=modbus_id)
self.__tcp_client.write_register(2716, 0, data_type=ModbusDataType.INT_32, unit=modbus_id)
self.last_mode = None
elif power_limit == 0:
log.debug("Aktive Batteriesteuerung. Batterie wird auf Stop gesetzt und nicht entladen")
if self.last_mode != 'stop':
# ESS Mode 3 für externe Steuerung und keine Entladung
self.__tcp_client.write_register(2902, 3, data_type=ModbusDataType.UINT_16, unit=modbus_id)
self.__tcp_client.write_register(39, 1, data_type=ModbusDataType.UINT_16, unit=vebus_id)
# ESS Mode 2 und Discharge Power 0% für externe Steuerung und keine Entladung
# Leistung an EVU-Punkt auf 0kW setzen -> Eigenregelung bei laden und Entladen verhindern
self.__tcp_client.write_register(2902, 2, data_type=ModbusDataType.UINT_16, unit=modbus_id)
self.__tcp_client.write_register(2702, 0, data_type=ModbusDataType.UINT_16, unit=modbus_id)

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im Branch power_limit == 0 wird nur 2702 (Discharge Power) auf 0% gesetzt, aber der EVU-Setpoint (2716) bleibt unverändert. Wenn vorher ein Lade-/Entlade-Setpoint gesetzt war, kann der Wechselrichter weiter aktiv auf diesen Zielwert regeln (insb. weiter laden), obwohl power_limit==0 in der Regelung als „kein Laden/Entladen“ verwendet wird. Vorschlag: beim Wechsel in den stop-Modus den Setpoint ebenfalls zurücksetzen bzw. passend berechnen (z.B. analog zu den anderen Zweigen set_power = (0 - current_power) + evu_power und 2716 schreiben), damit tatsächlich 0W Speicherleistung angestrebt wird.

Suggested change
self.__tcp_client.write_register(2702, 0, data_type=ModbusDataType.UINT_16, unit=modbus_id)
self.__tcp_client.write_register(2702, 0, data_type=ModbusDataType.UINT_16, unit=modbus_id)
# Beim Wechsel in den Stop-Modus auch den EVU-Setpoint so setzen,
# dass 0 W Speicherleistung angestrebt werden, analog zu den anderen Zweigen.
evu_power = data.data.counter_all_data.get_evu_counter().data.get.power
set_power = (0 - self.current_power) + evu_power
log.debug(
f"Aktive Batteriesteuerung Victron:"
f" Speicher wird gestoppt. \n"
f"Aktuelle Speicherleistung: {self.current_power} W, EVU-Leistung: {evu_power} W "
f"EVU-Leistung um {0 - self.current_power} W anpassen auf {set_power} W"
)
self.__tcp_client.write_register(
2716, set_power, data_type=ModbusDataType.INT_32, unit=modbus_id
)

Copilot uses AI. Check for mistakes.
self.__tcp_client.write_register(2716, 0, data_type=ModbusDataType.INT_32, unit=modbus_id)
self.last_mode = 'stop'
elif power_limit < 0:
evu_power = data.data.counter_all_data.get_evu_counter().data.get.power
set_power = (power_limit - self.current_power) + evu_power
log.debug(f"Aktive Batteriesteuerung Victron:"
Comment on lines 82 to +85

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.current_power is used in set_power_limit() but is only assigned in update(). If set_power_limit() is called before the first successful update(), this will raise an AttributeError and break battery control. Initialize self.current_power in initialize() (e.g., to 0) or fall back to the last stored battery power when it is not set yet.

Copilot uses AI. Check for mistakes.
f"Speicher soll mit {power_limit} W entladen werden. \n"
f"Aktuelle Speicherleistung: {self.current_power} W, EVU-Leistung: {evu_power} W "
f"EVU-Leistung um {power_limit - self.current_power} W anpassen auf {set_power} W")
if self.last_mode != 'discharge':
# ESS Mode 3 für externe Steuerung und auf L1 wird entladen
self.__tcp_client.write_register(2902, 3, data_type=ModbusDataType.UINT_16, unit=modbus_id)
self.__tcp_client.write_register(39, 0, data_type=ModbusDataType.UINT_16, unit=vebus_id)
self.__tcp_client.write_register(2902, 2, data_type=ModbusDataType.UINT_16, unit=modbus_id)
self.__tcp_client.write_register(2702, 100, data_type=ModbusDataType.UINT_16, unit=modbus_id)
self.last_mode = 'discharge'
# Die maximale Entladeleistung begrenzen auf 5000W
if phases == 3:
power_limit = power_limit / 3
power_value = int(min(power_limit, 5000))
log.debug(f"Aktive Batteriesteuerung. Victron mit {phases} Phase(n). "
f"Batterie wird mit {power_value} W pro Phase entladen.")

# Setzen der angestrebten EVU-Leistung, Speicher versucht seine Leistung
# anzupassen um den Zielwert zu erreichen
self.__tcp_client.write_register(
37, power_value & 0xFFFF, data_type=ModbusDataType.INT_16, unit=vebus_id)
if phases == 3:
self.__tcp_client.write_register(
40, power_value & 0xFFFF, data_type=ModbusDataType.INT_16, unit=vebus_id)
self.__tcp_client.write_register(
41, power_value & 0xFFFF, data_type=ModbusDataType.INT_16, unit=vebus_id)
2716, set_power, data_type=ModbusDataType.INT_32, unit=modbus_id)
elif power_limit > 0:
evu_power = data.data.counter_all_data.get_evu_counter().data.get.power
set_power = (power_limit - self.current_power) + evu_power
log.debug(f"Aktive Batteriesteuerung Victron:"
f"Speicher soll mit {power_limit} W geladen werden. \n"
f"Aktuelle Speicherleistung: {self.current_power} W, EVU-Leistung: {evu_power} W "
f"EVU-Leistung um {power_limit - self.current_power} W anpassen auf {set_power} W")
if self.last_mode != 'charge':
# ESS Mode 3 für externe Steuerung und auf L1 wird entladen
self.__tcp_client.write_register(2902, 3, data_type=ModbusDataType.UINT_16, unit=modbus_id)
self.__tcp_client.write_register(39, 0, data_type=ModbusDataType.UINT_16, unit=vebus_id)
self.__tcp_client.write_register(2902, 2, data_type=ModbusDataType.UINT_16, unit=modbus_id)
self.__tcp_client.write_register(2702, 100, data_type=ModbusDataType.UINT_16, unit=modbus_id)
self.last_mode = 'charge'
# Die maximale Entladeleistung begrenzen auf 5000W
if phases == 3:
power_limit = power_limit / 3
power_value = int(min(power_limit, 5000))
log.debug(f"Aktive Batteriesteuerung. Victron mit {phases} Phase(n). "
f"Batterie wird mit {power_value} W pro Phase geladen.")
self.__tcp_client.write_register(
37, power_value & 0xFFFF, data_type=ModbusDataType.INT_16, unit=vebus_id)
if phases == 3:
self.__tcp_client.write_register(
40, power_value & 0xFFFF, data_type=ModbusDataType.INT_16, unit=vebus_id)
self.__tcp_client.write_register(
41, power_value & 0xFFFF, data_type=ModbusDataType.INT_16, unit=vebus_id)
2716, set_power, data_type=ModbusDataType.INT_32, unit=modbus_id)

def power_limit_controllable(self) -> bool:
return True
Expand Down
3 changes: 1 addition & 2 deletions packages/modules/devices/victron/victron/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ def __init__(self,


class VictronBatConfiguration:
def __init__(self, modbus_id: int = 100, vebus_id: int = 228):
def __init__(self, modbus_id: int = 100):
self.modbus_id = modbus_id
self.vebus_id = vebus_id


class VictronBatSetup(ComponentSetup[VictronBatConfiguration]):
Expand Down
Loading