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
110 changes: 53 additions & 57 deletions packages/control/bat_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from control import data
from control.algorithm.chargemodes import CONSIDERED_CHARGE_MODES_CHARGING
from control.algorithm.filter_chargepoints import get_chargepoints_with_required_current_by_chargemode
from control.pv import Pv
from helpermodules.constants import NO_ERROR
from modules.common.abstract_device import AbstractDevice
from modules.common.component_context import SingleComponentUpdateContext
Expand Down Expand Up @@ -187,52 +186,35 @@ def calc_power_for_all_components(self):
except Exception:
log.exception("Fehler im Bat-Modul")

def get_bat_power_of_hybrid_system(self, inverter: Pv) -> float:
""" ermittelt die Leistung des Speichers, die über den Wechselrichter fließt, um die Leistung des Speichers
bei einem Hybrid-System zu berücksichtigen, wenn die maximale Ausgangsleistung des Wechselrichters erreicht ist.
"""
bat_power = 0
try:
children = data.data.counter_all_data.get_entry_of_element(inverter.num)["children"]
if len(children):
hybrid: List[str] = []
for c in children:
if c.get("type") == "bat":
hybrid.append(f'bat{c["id"]}')
break
if len(hybrid):
for bat in hybrid:
# nur wenn der Speicher entlädt, fließt Leistung über den WR
if data.data.bat_data[bat].data.get.power < 0:
bat_power += data.data.bat_data[bat].data.get.power
except Exception:
log.exception(f"Fehler im Bat-Modul {inverter.num}")
return bat_power

def _limit_bat_power_discharge(self, required_power: float) -> float:
"""begrenzt die für den Algorithmus benötigte Entladeleistung des Speichers, wenn die maximale Ausgangsleistung
des WR erreicht ist."""
if required_power > 0:
# Nur wenn der Speicher entladen werden soll, fließt Leistung durch den WR.
remaining_inverter_ac_out_power = 0
for inverter in data.data.pv_data.values():
def _absolute_bat_discharge_power(self) -> float:
discharge_power = 0
hybrid_bat_ids = data.data.counter_all_data.get_hybrid_bat_ids()
non_hybrid_bat_ids = data.data.counter_all_data.get_non_hybrid_bat_ids()
if len(hybrid_bat_ids) == 0:
# keine Hybrid-WR mit Speicher
discharge_power = float("inf")
else:
# nur Speicher an Hybrid-WR
discharge_power = 0
hybrid_inverter_ids = data.data.counter_all_data.get_hybrid_inverter_ids()
for inverter_id in hybrid_inverter_ids:
try:
if len(data.data.counter_all_data.get_entry_of_element(inverter.num)["children"]) == 0:
continue
bat_power = self.get_bat_power_of_hybrid_system(inverter)
inverter_power = max((inverter.data.get.power + bat_power) * -1, 0)
remaining_inverter_ac_out_power += inverter.data.config.max_ac_out - inverter_power + bat_power * -1
inverter = data.data.pv_data[f"pv{inverter_id}"]
inverter_power = max(inverter.data.get.power * -1, 0)
discharge_power += max(inverter.data.config.max_ac_out - inverter_power, 0)
except Exception:
log.exception(f"Fehler im Bat-Modul {inverter.num}")
if remaining_inverter_ac_out_power > 0:
required_power = min(required_power, remaining_inverter_ac_out_power)
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.")
return required_power
log.exception(f"Fehler im Bat-Modul {inverter_id}")
log.debug(f"Verbleibende Speicher-Leistung durch maximale Ausgangsleistung des Wechselrichters auf "
f"{discharge_power}W begrenzt.")
if len(non_hybrid_bat_ids) > 0:
# Speicher an Hybrid-WR und AC-Speicher im System
for bat_id in non_hybrid_bat_ids:
try:
bat = data.data.bat_data[f"bat{bat_id}"]
discharge_power += bat.data.get.max_discharge_power
except Exception:
log.exception(f"Fehler im Bat-Modul {bat_id}")
return discharge_power

def _set_bat_power_active_control(self, power):
controllable_bat_components, _ = get_bat_components_by_controllability()
Expand Down Expand Up @@ -326,7 +308,7 @@ def setup_bat(self):
if self.data.get.fault_state == 0:
self.get_power_limit()
self._set_bat_power_active_control(self.data.set.power_limit)
self._get_charging_power_left()
self.get_charging_power_left_diff()
log.info(f"{self.data.set.charging_power_left}W verbleibende Speicher-Leistung")
else:
# Bei Warnung oder Fehlerfall, zB durch Kalibrierung, Speicher-Leistung nicht in der
Expand All @@ -338,8 +320,9 @@ def setup_bat(self):
except Exception:
log.exception("Fehler im Bat-Modul")

def _get_charging_power_left(self):
""" ermittelt die Lade-Leistung des Speichers, die zum Laden der EV verwendet werden darf.
def get_charging_power_left_diff(self):
"""Ermittelt die Differenz zur aktuellen Batterie-Leistung,
die zum Laden der EV verwendet werden darf.
"""
try:
config = data.data.general_data.data.chargemode_config.pv_charging
Expand All @@ -364,6 +347,7 @@ def _get_charging_power_left(self):
else:
charging_power_left = self.data.get.power
else:
absolute_bat_discharge_power = self._absolute_bat_discharge_power()
# Speicher soll geladen werden um min SoC zu erreichen
if self.data.get.soc < config.min_bat_soc:
self.data.set.hysteresis_discharge = False
Expand All @@ -376,12 +360,10 @@ def _get_charging_power_left(self):
else:
# Speicher-Vorrang bis zum Min-Soc
if config.bat_power_reserve_active:
if self.data.get.power > config.bat_power_reserve:
# die Differenz darf nicht zum Laden der EV genutzt werden.
charging_power_left = self.data.get.power - config.bat_power_reserve
else:
charging_power_left = (
config.bat_power_reserve - self.data.get.power) * -1
# Die Differenz zwischen aktueller Batterie-Leistung und Reserveleistung bestimmt,
# was fuer EV-Ladung verbleibt (positiv) oder zusaetzlich benoetigt wird (negativ).
charging_power_left = self.data.get.power - config.bat_power_reserve
if charging_power_left < 0:
self.data.set.regulate_up = True
else:
# Speicher wird geladen
Expand Down Expand Up @@ -412,9 +394,16 @@ def _get_charging_power_left(self):
BatPowerLimitCondition.PRICE_LIMIT.value and
self.data.set.power_limit is None))
if config.bat_power_discharge_active and power_discharge_allowed:
# max Entladeleistung auf max Ausgangsleistung des WR begrenzen
required_absolut_discharge_power = min(
config.bat_power_discharge, absolute_bat_discharge_power)
# Differenz zwischen erlaubter Entladeleistung und aktueller Speicherleistung bestimmen.
# Wenn der Speicher lädt, darf die freigegebene Entladeleistung nicht die max
# Ausgangsleistung des WR überschreiten.
# Wenn der Speicher mit mehr als der erlaubten Entladeleistung entladen wird, muss das
# vom Überschuss subtrahiert werden.
charging_power_left = config.bat_power_discharge + base_power
charging_power_left = min(required_absolut_discharge_power + base_power,
absolute_bat_discharge_power)
log.debug(f"Erlaubte Entlade-Leistung nutzen {charging_power_left}W")
else:
# Speicher sollte weder ge- noch entladen werden.
Expand Down Expand Up @@ -442,9 +431,16 @@ def _get_charging_power_left(self):
BatPowerLimitCondition.PRICE_LIMIT.value and
self.data.set.power_limit is None))
if config.bat_power_discharge_active and power_discharge_allowed:
# max Entladeleistung auf max Ausgangsleistung des WR begrenzen
required_absolut_discharge_power = min(
config.bat_power_discharge, absolute_bat_discharge_power)
# Differenz zwischen erlaubter Entladeleistung und aktueller Speicherleistung bestimmen.
# Wenn der Speicher lädt, darf die freigegebene Entladeleistung nicht die max
# Ausgangsleistung des WR überschreiten.
# Wenn der Speicher mit mehr als der erlaubten Entladeleistung entladen wird, muss das
# vom Überschuss subtrahiert werden.
charging_power_left = config.bat_power_discharge + base_power
charging_power_left = min(required_absolut_discharge_power + base_power,
absolute_bat_discharge_power)
log.debug(f"Erlaubte Entlade-Leistung nutzen {charging_power_left}W")
else:
# Speicher sollte weder ge- noch entladen werden.
Expand All @@ -461,7 +457,7 @@ def _get_charging_power_left(self):
log.debug("Damit der Speicher hochregeln kann, muss unabhängig vom eingestellten Regelmodus "
"Einspeisung erzeugt werden.")
charging_power_left -= 100
self.data.set.charging_power_left = self._limit_bat_power_discharge(charging_power_left)
self.data.set.charging_power_left = charging_power_left
except Exception:
log.exception("Fehler im Bat-Modul")

Expand Down
102 changes: 77 additions & 25 deletions packages/control/bat_all_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from control import bat_all
from control.bat import Bat

from control.bat_all import BatAll, BatPowerLimitMode, BatPowerLimitCondition, ManualMode
from control.bat_all import BatAll, BatConsiderationMode, BatPowerLimitMode, BatPowerLimitCondition, ManualMode
from control import data
from control.chargepoint.chargepoint import Chargepoint
from control.chargepoint.chargepoint_all import AllChargepointData, AllChargepoints, AllGet
Expand All @@ -27,55 +27,73 @@ def data_fixture() -> None:


@pytest.mark.parametrize(
"bat_power, required_power, pv_power, expected_power",
"bat_power, pv_power, expected_power",
[
pytest.param(-1000, 1000, 0, 1000, id="Leistung verfügbar"),
pytest.param(-4900, 5100, -100, 4900, id="max Leistung des WR um 100W überschritten, Speicher entlädt"),
pytest.param(1000, 1600, -4500, 500, id="Speicher lädt, soll entladen"),
pytest.param(-1000, 0, 4000, id="Leistung verfügbar"),
pytest.param(-4900, -100, 0, id="max Leistung des WR um 100W überschritten, Speicher entlädt"),
pytest.param(1000, -4500, 500, id="Speicher lädt, soll entladen"),
])
def test_limit_bat_power_discharge(bat_power: int,
required_power: int,
pv_power: int,
expected_power: int,
monkeypatch: pytest.MonkeyPatch):
def test_get_charging_power_left_diff_hybrid(bat_power: int,
pv_power: int,
expected_power: int,
monkeypatch: pytest.MonkeyPatch):
# setup
data.data.pv_data = {"pv2": Pv(2)}
data.data.pv_data["pv2"].data.get.power = pv_power
data.data.pv_data["pv2"].data.config.max_ac_out = 5000
data.data.bat_data["bat1"] = Bat(1)
data.data.bat_data["bat1"].data.get.power = bat_power
mock_entry_children = Mock(return_value={"id": 5, "type": "pv", "children": [
{"id": 1, "type": "bat", "children": []}]})
monkeypatch.setattr(data.data.counter_all_data, "get_entry_of_element", mock_entry_children)
data.data.bat_data["bat1"].data.get.soc = 71
data.data.general_data.data.chargemode_config.pv_charging.bat_mode = BatConsiderationMode.MIN_SOC_BAT.value
data.data.general_data.data.chargemode_config.pv_charging.bat_power_discharge = 5000
data.data.general_data.data.chargemode_config.pv_charging.bat_power_discharge_active = True
monkeypatch.setattr(data.data.counter_all_data, "get_hybrid_bat_ids", Mock(return_value=[1]))
monkeypatch.setattr(data.data.counter_all_data, "get_non_hybrid_bat_ids", Mock(return_value=[]))
monkeypatch.setattr(data.data.counter_all_data, "get_hybrid_inverter_ids", Mock(return_value=[2]))

b = BatAll()
b.data.get.power = bat_power
b_all = BatAll()
b_all.data.get.power = bat_power
b_all.data.get.soc = 71

# execution
power = b._limit_bat_power_discharge(required_power) # pyright: ignore[reportPrivateUsage]
b_all.get_charging_power_left_diff()

# evaluation
assert power == expected_power
assert b_all.data.set.charging_power_left == expected_power


def test_limit_bat_power_discharge_no_hybrid_system(monkeypatch: pytest.MonkeyPatch):
@pytest.mark.parametrize(
"hybrid_bat_ids, non_hybrid_bat_ids, hybrid_inverter_ids, expected_power",
[
pytest.param([], [1], [], float("inf"), id="no hybrid"),
pytest.param([1], [], [2], 4900, id="hybrid,"),
pytest.param([1], [3], [2], 10900, id="hybrid an non hybrid bat"),
])
def test__absolute_bat_discharge_power(hybrid_bat_ids: List[int],
non_hybrid_bat_ids: List[int],
hybrid_inverter_ids: List[int],
expected_power: float,
monkeypatch: pytest.MonkeyPatch):
# setup
data.data.pv_data = {"pv2": Pv(2)}
data.data.pv_data["pv2"].data.get.power = -100
data.data.pv_data["pv2"].data.config.max_ac_out = 5000
data.data.bat_data["bat1"] = Bat(1)
data.data.bat_data["bat1"].data.get.power = -4900
mock_entry_children = Mock(return_value={"id": 5, "type": "pv", "children": []})
monkeypatch.setattr(data.data.counter_all_data, "get_entry_of_element", mock_entry_children)
data.data.bat_data["bat3"] = Bat(3)
data.data.bat_data["bat3"].data.get.max_discharge_power = 6000
monkeypatch.setattr(data.data.counter_all_data, "get_hybrid_bat_ids", Mock(return_value=hybrid_bat_ids))
monkeypatch.setattr(data.data.counter_all_data, "get_non_hybrid_bat_ids", Mock(return_value=non_hybrid_bat_ids))
monkeypatch.setattr(data.data.counter_all_data, "get_hybrid_inverter_ids", Mock(return_value=hybrid_inverter_ids))

b = BatAll()
b.data.get.power = -4900

# execution
power = b._limit_bat_power_discharge(5100) # pyright: ignore[reportPrivateUsage]
power = b._absolute_bat_discharge_power() # pyright: ignore[reportPrivateUsage]

# evaluation
assert power == 5100
assert power == expected_power


@dataclass
Expand Down Expand Up @@ -159,17 +177,51 @@ def test_get_charging_power_left(params: Params, caplog, data_, monkeypatch):
b.data.get.power = params.power
data.data.bat_data["bat0"] = b
data.data.general_data.data.chargemode_config.pv_charging = params.config
mock_limit_bat_power_discharge = MagicMock(side_effect=lambda x: x)
monkeypatch.setattr(BatAll, "_limit_bat_power_discharge", mock_limit_bat_power_discharge)
mock_absolute_bat_discharge_power = MagicMock(return_value=10000)
monkeypatch.setattr(BatAll, "_absolute_bat_discharge_power", mock_absolute_bat_discharge_power)

# execution
b_all._get_charging_power_left()
b_all.get_charging_power_left_diff()

# evaluation
assert b_all.data.set.charging_power_left == params.expected_charging_power_left
assert b_all.data.set.regulate_up == params.expected_regulate_up


def test_get_charging_power_left_uses_limited_bat_discharge_in_hysteresis(
data_: data.Data, monkeypatch: pytest.MonkeyPatch):
# setup: min/max-SoC-Bereich mit aktiver Hysterese und erlaubter Entladeleistung
b_all = BatAll()
b_all.data.get.power = -2500
b_all.data.get.soc = 60
b_all.data.set.hysteresis_discharge = True
b_all.data.set.power_limit = None
data.data.general_data.data.chargemode_config.pv_charging = PvCharging(
bat_mode="min_soc_bat_mode",
min_bat_soc=40,
max_bat_soc=80,
bat_power_discharge=8000,
bat_power_discharge_active=True,
)

# Hybrid-Setup fuer reale Berechnung in _limit_bat_power_discharge
data.data.pv_data = {"pv2": Pv(2)}
data.data.pv_data["pv2"].data.get.power = -7500
data.data.pv_data["pv2"].data.config.max_ac_out = 10000
data.data.bat_data["bat1"] = Bat(1)
data.data.bat_data["bat1"].data.get.power = -2500
monkeypatch.setattr(data.data.counter_all_data, "get_hybrid_bat_ids", Mock(return_value=[1]))
monkeypatch.setattr(data.data.counter_all_data, "get_non_hybrid_bat_ids", Mock(return_value=[]))
monkeypatch.setattr(data.data.counter_all_data, "get_hybrid_inverter_ids", Mock(return_value=[2]))

# execution
b_all.get_charging_power_left_diff()

# evaluation: reale Begrenzung (300W) + base_power (400W)
assert b_all.data.set.charging_power_left == 0
assert b_all.data.set.regulate_up is False


def default_chargepoint_factory() -> List[Chargepoint]:
cp = Chargepoint(3, None)
cp.data.get.power = 1400
Expand Down
Loading