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
32 changes: 23 additions & 9 deletions packages/control/bat_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ def calc_power_for_all_components(self):
except Exception:
log.exception("Fehler im Bat-Modul")

def _inverter_limited_power(self, inverter: Pv) -> float:
"""gibt die maximale Entladeleistung des Speichers zurück, bis die maximale Ausgangsleistung des WR erreicht
ist."""
def _get_pv_power_beyond_max_ac_out(self, inverter: Pv) -> float:
"""gibt die PV-Leistung zurück, die über der maximalen Ausgangsleistung des Wechselrichters liegt und somit
nicht für die Entladung des Speichers genutzt werden kann."""
Comment on lines +183 to +184
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Im Docstring ist ein Tippfehler: „Entadung“ sollte „Entladung“ heißen.

Copilot uses AI. Check for mistakes.
# tested
Comment on lines +182 to 185
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The docstring for _get_pv_power_beyond_max_ac_out says it returns “die maximale Entladeleistung des Speichers …”, but the implementation returns PV power that exceeds max_ac_out (based on inverter.data.get.power and max_ac_out). Updating the docstring (and optionally the inline comment) to describe the returned value will avoid confusion for future maintenance.

Copilot uses AI. Check for mistakes.
# Wenn vom PV-Ertrag der Speicher geladen wird, kann diese Leistung bis zur max Ausgangsleistung des WR
# genutzt werden.
Expand All @@ -193,18 +193,32 @@ def _inverter_limited_power(self, inverter: Pv) -> float:
def _limit_bat_power_discharge(self, required_power):
"""begrenzt die für den Algorithmus benötigte Entladeleistung des Speichers, wenn die maximale Ausgangsleistung
des WR erreicht ist."""
inverter_limited_power = 0
pv_power_beyond_max_ac_out = 0
if required_power > 0:
# Nur wenn der Speicher entladen werden soll, fließt Leistung durch den WR.
for inverter in data.data.pv_data.values():
try:
inverter_limited_power += self._inverter_limited_power(inverter)
pv_power_beyond_max_ac_out += self._get_pv_power_beyond_max_ac_out(inverter)
except Exception:
log.exception(f"Fehler im Bat-Modul {inverter.num}")
if inverter_limited_power > 0:
required_power = max(required_power-inverter_limited_power, 0)
log.debug(f"Verbleibende Speicher-Leistung durch maximale Ausgangsleistung auf {required_power}W"
" begrenzt.")
# Wenn max_inverter_power_for_bat nur deshalb negativ ist, weil der Speicher aktuell bereits entlädt
# (self.data.get.power < 0) und keine PV-Leistung über der maximalen WR-Ausgangsleistung anliegt
# (pv_power_beyond_max_ac_out == 0), dann würde eine Begrenzung auf 0W die noch verfügbare
# Entladeleistung fälschlicherweise unterdrücken. In diesem Fall wenden wir keine zusätzliche
# Begrenzung durch die WR-Ausgangsleistung an.
if pv_power_beyond_max_ac_out > 0:
max_inverter_power_for_bat = self.data.get.power - pv_power_beyond_max_ac_out
# Negative Werte bedeuten, dass bereits mehr Leistung über den WR fließt, als für den Speicher
# zusätzlich verfügbar ist; in diesem Fall ist keine weitere Entladung möglich.
max_inverter_power_for_bat = max(max_inverter_power_for_bat, 0)
required_power = min(required_power, max_inverter_power_for_bat)
log.debug(
f"Verbleibende Speicher-Leistung durch maximale Ausgangsleistung auf {required_power}W begrenzt."
)
else:
log.debug(
"Speicher-Entladeleistung nicht durch maximale WR-Ausgangsleistung begrenzt, da keine PV-Leistung "
"über der maximalen WR-Ausgangsleistung anliegt.")
return required_power

def _set_bat_power_active_control(self, power):
Expand Down
32 changes: 21 additions & 11 deletions packages/control/bat_all_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@
from modules.devices.generic.mqtt.config import MqttBatSetup


@pytest.fixture
@pytest.fixture(autouse=True)
def data_fixture() -> None:
data.data_init(Mock())
data.data.general_data = General()
Comment on lines +19 to 21
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

In diesem Testmodul gibt es bereits das data_-Fixture aus packages/conftest.py, das ebenfalls data.data_init(...) aufruft. Das neue autouse=True-Fixture initialisiert data damit für jeden Test doppelt (für Tests mit data_) und macht die Ausführungsreihenfolge der Fixtures relevant. Besser: dieses Fixture nicht als autouse verwenden, sondern data_ in den betroffenen Tests (z.B. test_pv_power_beyond_max_ac_out, test_limit_bat_power_discharge) explizit anfordern oder data_fixture von data_ abhängig machen und kein eigenes data.data_init(...) mehr ausführen.

Copilot uses AI. Check for mistakes.
data.data.cp_all_data = Mock(spec=AllChargepoints, data=Mock(
spec=AllChargepointData, get=Mock(spec=AllGet, power=0)))
Expand All @@ -35,34 +34,45 @@ def data_fixture() -> None:
pytest.param(0, -6000, 0, id="max_ac_out ist 0"),
],
)
def test_inverter_limited_power(max_ac_out, power, expected_result):
def test_pv_power_beyond_max_ac_out(max_ac_out: int, power: int, expected_result: int):
# Mock für die Pv-Klasse
inverter = Pv(1)
inverter.data.config.max_ac_out = max_ac_out
inverter.data.get.power = power
bat_all = BatAll()

# Aufruf der zu testenden Funktion
result = bat_all._inverter_limited_power(inverter)
result = bat_all._get_pv_power_beyond_max_ac_out(inverter)

# Überprüfung des Ergebnisses
assert result == expected_result


@pytest.mark.parametrize(
"required_power, return_inverter_limited_power, expected_power",
"bat_power, required_power, return_pv_power_beyond_max_ac_out, expected_power",
[
pytest.param(1000, 0, 1000, id="maximale Entladeleistung nicht erreicht"),
pytest.param(1000, 100, 900, id="maximale Entladeleistung erreicht"),
pytest.param(-1000, 10, -1000, id="Speicher soll nicht mehr entladen werden"),
pytest.param(1000, 1000, 0, 1000, id="exakt max Leistung des WR"),
pytest.param(1000, 1000, 100, 900, id="max Leistung des WR um 100W überschritten"),
pytest.param(1000, 1000, 1100, 0, id="maximale Entladeleistung erreicht"),
pytest.param(3000, 3000, 2000, 1000, id="max Leistung des WR um 2000W überschritten"),
pytest.param(3000, 5000, 2000, 1000, id="max Leistung des WR um 2000W überschritten, " +
"erlaubte Entladeleistung höher als aktuelle Leistung"),
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

test_limit_bat_power_discharge currently only exercises b.data.get.power values >= 0. _limit_bat_power_discharge can be invoked with required_power > 0 even when b.data.get.power < 0 (battery discharging but still with remaining allowed discharge headroom), and the new clamp logic is sensitive to that sign. Please add at least one parametrized case covering bat_power < 0 with required_power > 0 to guard against regressions.

Suggested change
"erlaubte Entladeleistung höher als aktuelle Leistung"),
"erlaubte Entladeleistung höher als aktuelle Leistung"),
pytest.param(-1000, 1000, 2000, 0, id="Speicher lädt, Entladung angefordert, PV bereits über max_ac_out"),

Copilot uses AI. Check for mistakes.
pytest.param(-1000, 1100, 0, 1100, id="Speicher entlädt, soll entladen"),
pytest.param(-1000, -600, 0, -600, id="Speicher entlädt, soll weniger entladen"),
pytest.param(0, 600, 0, 600, id="Speicher ruht, soll entladen"),
])
def test_limit_bat_power_discharge(required_power, return_inverter_limited_power, expected_power, monkeypatch):
def test_limit_bat_power_discharge(bat_power: int,
required_power: int,
return_pv_power_beyond_max_ac_out: int,
expected_power: int,
monkeypatch):
# setup
data.data.pv_data = {"pv2": Pv(2)}
mock_inverter_limited_power = Mock(return_value=return_inverter_limited_power)
monkeypatch.setattr(BatAll, "_inverter_limited_power", mock_inverter_limited_power)
mock_pv_power_beyond_max_ac_out = Mock(return_value=return_pv_power_beyond_max_ac_out)
monkeypatch.setattr(BatAll, "_get_pv_power_beyond_max_ac_out", mock_pv_power_beyond_max_ac_out)
Comment on lines 69 to +72
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

data.data.pv_data = {"pv2": Pv(2)} assumes the global control.data.data has been initialized via data.data_init(...), but this test doesn’t request the local data_fixture (and doesn’t take the shared data_ fixture either). That makes the test order-dependent and it can fail in isolation. Include a fixture that calls data.data_init(...) in this test’s signature (or make the module’s data_fixture autouse).

Copilot uses AI. Check for mistakes.

b = BatAll()
b.data.get.power = bat_power

# execution
power = b._limit_bat_power_discharge(required_power)
Expand Down
Loading