From 07739558cecdb3c7bec591c828679f3bdbfd212c Mon Sep 17 00:00:00 2001 From: Xoffroad Date: Wed, 25 Mar 2026 11:36:52 +0100 Subject: [PATCH 1/3] adding active battery control for qcells and new field for overload security --- packages/modules/devices/qcells/qcells/config.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/modules/devices/qcells/qcells/config.py b/packages/modules/devices/qcells/qcells/config.py index 445e253b07..0b28e97f7d 100644 --- a/packages/modules/devices/qcells/qcells/config.py +++ b/packages/modules/devices/qcells/qcells/config.py @@ -25,8 +25,13 @@ def __init__(self, class QCellsBatConfiguration: - def __init__(self): - pass + def __init__(self, max_power: int = 4000): + # Maximale Lade-/Entladeleistung des Speichers in Watt. + # Speichersteuerung via Solax Remote Control Mode 8 (Modbus). + # Unterstuetzte Hardware: QCells Q.VOLT HYB-G3-3P (Solax Gen4), + # Solax Gen4/Gen5/Gen6 Hybrid und AC Wechselrichter. + # Gen2/Gen3 werden nicht unterstuetzt (kein Remote Control). + self.max_power = max_power class QCellsBatSetup(ComponentSetup[QCellsBatConfiguration]): From c2ccbe5bec52e670d67482a7097906a881dbf9e5 Mon Sep 17 00:00:00 2001 From: Xoffroad Date: Thu, 26 Mar 2026 15:24:30 +0100 Subject: [PATCH 2/3] remove field for max_power and use max_charge_power and max_discharge_power instead --- packages/modules/devices/qcells/qcells/config.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/modules/devices/qcells/qcells/config.py b/packages/modules/devices/qcells/qcells/config.py index 0b28e97f7d..445e253b07 100644 --- a/packages/modules/devices/qcells/qcells/config.py +++ b/packages/modules/devices/qcells/qcells/config.py @@ -25,13 +25,8 @@ def __init__(self, class QCellsBatConfiguration: - def __init__(self, max_power: int = 4000): - # Maximale Lade-/Entladeleistung des Speichers in Watt. - # Speichersteuerung via Solax Remote Control Mode 8 (Modbus). - # Unterstuetzte Hardware: QCells Q.VOLT HYB-G3-3P (Solax Gen4), - # Solax Gen4/Gen5/Gen6 Hybrid und AC Wechselrichter. - # Gen2/Gen3 werden nicht unterstuetzt (kein Remote Control). - self.max_power = max_power + def __init__(self): + pass class QCellsBatSetup(ComponentSetup[QCellsBatConfiguration]): From 24e2d0fbb350fe0e5dfe8eefc14a7adadf200bd6 Mon Sep 17 00:00:00 2001 From: Xoffroad Date: Tue, 21 Apr 2026 15:27:38 +0200 Subject: [PATCH 3/3] fix: stabilize QCells active battery control in mode 1 Reworked QCells battery control to a stable Mode 1 workflow aligned with the validated HA behavior. The target calculation was corrected to avoid unintended forced charging, import limiting was applied consistently for net import control, and the full Mode 1 control payload is now written each cycle with dynamic timing based on the configured openWB control interval. --- packages/modules/devices/qcells/qcells/bat.py | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/modules/devices/qcells/qcells/bat.py b/packages/modules/devices/qcells/qcells/bat.py index 0db941fd47..13b1cbb9f7 100644 --- a/packages/modules/devices/qcells/qcells/bat.py +++ b/packages/modules/devices/qcells/qcells/bat.py @@ -21,7 +21,11 @@ REMOTE_CONTROL_MODE_REG = 0x7C # U16: 0=Disabled, 1=Enabled Power Control REMOTE_CONTROL_SET_TYPE_REG = 0x7D # U16: 1=Set REMOTE_CONTROL_ACTIVE_POWER_REG = 0x7E # S32: Active Power Sollwert in Watt +REMOTE_CONTROL_REACTIVE_POWER_REG = 0x80 # S32: Reactive Power Sollwert (0) REMOTE_CONTROL_DURATION_REG = 0x82 # U16: Dauer in Sekunden +REMOTE_CONTROL_TARGET_SOC_REG = 0x83 # U16: Target SoC (Dummy 0) +REMOTE_CONTROL_TARGET_ENERGY_REG = 0x84 # U32: Target Energy (Dummy 0) +REMOTE_CONTROL_TARGET_POWER_REG = 0x86 # S32: Target Charge/Discharge Power (Dummy 0) REMOTE_CONTROL_TIMEOUT_REG = 0x88 # U16: Timeout in Sekunden MODE_1_DISABLED = 0 @@ -97,7 +101,8 @@ def _get_active_power_target(self, power_limit: int) -> int: cp_power = int(data.data.cp_all_data.data.get.power) house_load = max(0, home_consumption + cp_power) pv_generation = max(0, int(data.data.pv_all_data.data.get.power * -1)) - ap_target = power_limit + house_load - pv_generation + # Mode 1 / Enabled Battery Control: house_load wird inverter-intern bereits berücksichtigt. + ap_target = power_limit - pv_generation try: evu_counter = data.data.counter_all_data.get_evu_counter() @@ -105,19 +110,27 @@ def _get_active_power_target(self, power_limit: int) -> int: except Exception: import_limit = 0 - if import_limit > 0: - ap_target = min(ap_target, import_limit) + import_bound = None + if import_limit > 0 and ap_target > 0: + import_bound = import_limit - house_load + ap_target = min(ap_target, import_bound) log.debug(( f"QCells Mode1 target: power_limit={power_limit}W, home_consumption={home_consumption}W, " f"cp_power={cp_power}W, house_load={house_load}W, " - f"pv_generation={pv_generation}W, import_limit={import_limit}W -> ap_target={ap_target}W" + f"pv_generation={pv_generation}W, import_limit={import_limit}W, " + f"import_bound={import_bound}W -> ap_target={ap_target}W" )) return int(ap_target) def _write_mode1(self, ap_target: int, unit: int) -> None: """Schreibt die Mode 1 Remote Control Register (0x7C-0x88).""" duration, timeout = self._get_mode1_timing() + log.debug(( + f"QCells Mode1 write: mode={MODE_1_ENABLED_POWER_CONTROL}, set_type={SET_TYPE_SET}, " + f"active_power={ap_target}W, reactive_power=0var, duration={duration}s, " + f"target_soc=0, target_energy=0Wh, target_power=0W, timeout={timeout}s" + )) with self.client: self.client.write_register( REMOTE_CONTROL_MODE_REG, MODE_1_ENABLED_POWER_CONTROL, @@ -128,9 +141,21 @@ def _write_mode1(self, ap_target: int, unit: int) -> None: self.client.write_register( REMOTE_CONTROL_ACTIVE_POWER_REG, ap_target, data_type=ModbusDataType.INT_32, wordorder=Endian.Little, unit=unit) + self.client.write_register( + REMOTE_CONTROL_REACTIVE_POWER_REG, 0, + data_type=ModbusDataType.INT_32, wordorder=Endian.Little, unit=unit) self.client.write_register( REMOTE_CONTROL_DURATION_REG, duration, data_type=ModbusDataType.UINT_16, unit=unit) + self.client.write_register( + REMOTE_CONTROL_TARGET_SOC_REG, 0, + data_type=ModbusDataType.UINT_16, unit=unit) + self.client.write_register( + REMOTE_CONTROL_TARGET_ENERGY_REG, 0, + data_type=ModbusDataType.UINT_32, wordorder=Endian.Little, unit=unit) + self.client.write_register( + REMOTE_CONTROL_TARGET_POWER_REG, 0, + data_type=ModbusDataType.INT_32, wordorder=Endian.Little, unit=unit) self.client.write_register( REMOTE_CONTROL_TIMEOUT_REG, timeout, data_type=ModbusDataType.UINT_16, unit=unit)