Skip to content
Draft
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
9 changes: 9 additions & 0 deletions packages/control/algorithm/surplus_controlled.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ def check_submode_pv_charging(self) -> None:
for load in get_loads_by_chargemodes(CONSIDERED_CHARGE_MODES_PV_ONLY):
try:
def phase_switch_necessary() -> bool:
if data.data.bat_all_data.is_buffer_depleted():
# Der Speicher-Puffer ist leer und der Speicher hat bis zum Mindest-SoC
# Vorrang. Die Rückschaltung 3 -> 1 ist dann nicht mehr die richtige
# Reaktion, sondern das Abschalten - sonst stapeln sich switch_off_delay
# (Rückschaltung), phase_switch_delay und noch einmal switch_off_delay
# (Abschaltung), und die Ladung laeuft die ganze Zeit aus dem Netz.
# Ev._check_phase_switch_conditions() unterdrückt die Rückschaltung
# passend dazu.
return False
return isinstance(load, Chargepoint) and (load.cp_state_hw_support_phase_switch() and
load.data.get.phases_in_use != 1 and
load.data.control_parameter.template_phases == 0)
Expand Down
37 changes: 37 additions & 0 deletions packages/control/algorithm/surplus_controlled_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from control.chargemode import Chargemode
from control.chargepoint.chargepoint import Chargepoint, ChargepointData
from control.chargepoint.chargepoint_data import Get, Set
from control.chargepoint.chargepoint_state import ChargepointState
from control.chargepoint.chargepoint_template import CpTemplate
from control.chargepoint.control_parameter import ControlParameter
from control.counter_all.counter_all import CounterAll
Expand Down Expand Up @@ -158,3 +159,39 @@ def setup_cp(cp: Chargepoint, submode: str) -> Chargepoint:

# assertion
assert chargepoints == expected_chargepoints


@pytest.mark.parametrize(
"is_buffer_depleted, expected_switch_off_checked",
[
pytest.param(True, True,
id="Puffer leer -> Abschaltschwelle greift trotz möglicher Rückschaltung"),
pytest.param(False, False,
id="Puffer nicht leer -> Rückschaltung hat wie bisher Vorrang"),
])
def test_check_submode_pv_charging_buffer_depleted(is_buffer_depleted: bool,
expected_switch_off_checked: bool,
mock_cp1: Chargepoint,
monkeypatch):
# setup: dreiphasig ladender LP im Automatikmodus, dessen Hardware umschalten kann
mock_cp1.data = ChargepointData()
mock_cp1.data.set.charging_ev_data = Ev(1)
mock_cp1.data.config.ev = 1
mock_cp1.data.control_parameter.chargemode = Chargemode.PV_CHARGING
mock_cp1.data.control_parameter.submode = Chargemode.PV_CHARGING
mock_cp1.data.control_parameter.state = ChargepointState.CHARGING_ALLOWED
mock_cp1.data.control_parameter.template_phases = 0
mock_cp1.data.get.phases_in_use = 3
monkeypatch.setattr(Chargepoint, "cp_state_hw_support_phase_switch", Mock(return_value=True))
data.data.cp_data = {"cp1": mock_cp1}
data.data.counter_all_data.data.get.loadmanagement_prios = [{"type": "vehicle", "id": 1}]
switch_off_check_threshold_mock = Mock()
evu_counter_mock = Mock(switch_off_check_threshold=switch_off_check_threshold_mock)
monkeypatch.setattr(data.data.counter_all_data, "get_evu_counter", Mock(return_value=evu_counter_mock))
monkeypatch.setattr(data.data.bat_all_data, "is_buffer_depleted", Mock(return_value=is_buffer_depleted))

# execution
SurplusControlled().check_submode_pv_charging()

# evaluation
assert switch_off_check_threshold_mock.called == expected_switch_off_checked
64 changes: 64 additions & 0 deletions packages/control/bat_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,70 @@ def get_charging_power_left_diff(self):
except Exception:
log.exception("Fehler im Bat-Modul")

def _buffer_configured(self) -> bool:
""" gibt zurück, ob der Speicher überhaupt als Puffer für die Ladung wirken kann.

Nur im Modus "Mindest-SoC des Speichers" und nur, wenn eine Entladeleistung für die
Fahrzeuge freigegeben ist. Die Prüfung der Freigabe entspricht der in
get_charging_power_left_diff(), wo die erlaubte Entladeleistung dem Überschuss
zugeschlagen wird.
"""
if self.data.config.configured is False or self.data.get.fault_state != 0:
return False
config = data.data.general_data.data.chargemode_config.bat
if config.mode != BatConsiderationMode.MIN_SOC_BAT.value:
return False
# Aktive Steuerung nicht konfiguriert oder
# Aktive Steuerung + Preisgrenze aktiv + Grenze nicht unterschritten
# -> dann wird die erlaubte Speicherentladeleistung addiert
power_discharge_allowed = (self.data.config.bat_control_activated is False or
(self.data.config.power_limit_condition ==
BatPowerLimitCondition.PRICE_LIMIT.value and
self.data.set.power_limit is None))
return config.power_discharge_active and power_discharge_allowed

def is_buffering(self) -> bool:
""" gibt zurück, ob der Speicher die Überschuss-Ladung gerade puffert.

Wahr auf dem absteigenden Ast der Hysterese: der Maximal-SoC wurde erreicht
(hysteresis_discharge), der Mindest-SoC noch nicht wieder unterschritten. Genau in diesem
Zustand gibt get_charging_power_left_diff() die erlaubte Entladeleistung als Überschuss
frei, damit der Speicher eine laufende Ladung über Wolken hinweg stützt.

hysteresis_discharge wird nur im Mindest-SoC-Modus gepflegt und ist in den anderen Modi ein
Altwert, deshalb die Modus-Prüfung in _buffer_configured().

Return
------
bool: Der Speicher stützt gerade die Ladung.
"""
try:
return (self._buffer_configured() and
self.data.set.hysteresis_discharge and
self.data.set.power_limit is None)
except Exception:
log.exception("Fehler im Bat-Modul")
return False

def is_buffer_depleted(self) -> bool:
""" gibt zurück, ob der Puffer aufgebraucht ist und der Speicher Vorrang hat.

Unterhalb des Mindest-SoC gibt get_charging_power_left_diff() keine Entladeleistung mehr
frei, setzt hysteresis_discharge zurück und regulate_up, damit der Speicher sich wieder
aufladen kann ("Speicher-Vorrang bis zum Min-Soc"). Eine laufende Fahrzeug-Ladung
konkurriert ab hier direkt mit dem Speicher.

Return
------
bool: Der Puffer ist leer, der Speicher lädt sich wieder auf.
"""
try:
config = data.data.general_data.data.chargemode_config.bat
return self._buffer_configured() and self.data.get.soc < config.min_soc
except Exception:
log.exception("Fehler im Bat-Modul")
return False

def power_for_bat_charging(self):
""" gibt die Leistung zurück, die zum Laden verwendet werden kann.

Expand Down
74 changes: 74 additions & 0 deletions packages/control/bat_all_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,3 +524,77 @@ def test_time_charging_min_bat_soc_allowed_pricing(ep_configured: bool,

# evaluation
assert result == expected_result


@pytest.mark.parametrize(
"configured, fault_state, mode, hysteresis_discharge, power_discharge_active, power_limit, expected_result",
[
pytest.param(True, 0, BatConsiderationMode.MIN_SOC_BAT.value, True, True, None, True,
id="Mindest-SoC-Modus, Hysterese aktiv, Entladeleistung freigegeben -> puffert"),
pytest.param(True, 0, BatConsiderationMode.MIN_SOC_BAT.value, False, True, None, False,
id="Hysterese nicht aktiv -> puffert nicht"),
pytest.param(True, 0, BatConsiderationMode.MIN_SOC_BAT.value, True, False, None, False,
id="keine Entladeleistung freigegeben -> puffert nicht"),
pytest.param(True, 0, BatConsiderationMode.MIN_SOC_BAT.value, True, True, 2000, False,
id="Speicher wird mit vorgegebener Leistung entladen -> puffert nicht"),
pytest.param(True, 0, BatConsiderationMode.BAT_MODE.value, True, True, None, False,
id="Speicher-Modus, hysteresis_discharge ist dort ein Altwert -> puffert nicht"),
pytest.param(True, 0, BatConsiderationMode.EV_MODE.value, True, True, None, False,
id="EV-Modus, hysteresis_discharge ist dort ein Altwert -> puffert nicht"),
pytest.param(True, 2, BatConsiderationMode.MIN_SOC_BAT.value, True, True, None, False,
id="Fehlerfall, Speicher nicht in der Regelung -> puffert nicht"),
pytest.param(False, 0, BatConsiderationMode.MIN_SOC_BAT.value, True, True, None, False,
id="kein Speicher konfiguriert -> puffert nicht"),
])
def test_is_buffering(configured: bool,
fault_state: int,
mode: str,
hysteresis_discharge: bool,
power_discharge_active: bool,
power_limit: Optional[float],
expected_result: bool):
# setup
b = BatAll()
b.data.config.configured = configured
b.data.config.bat_control_activated = False
b.data.get.fault_state = fault_state
b.data.set.hysteresis_discharge = hysteresis_discharge
b.data.set.power_limit = power_limit
data.data.general_data.data.chargemode_config.bat.mode = mode
data.data.general_data.data.chargemode_config.bat.power_discharge_active = power_discharge_active

# execution
result = b.is_buffering()

# evaluation
assert result == expected_result


@pytest.mark.parametrize(
"mode, soc, power_discharge_active, expected_result",
[
pytest.param(BatConsiderationMode.MIN_SOC_BAT.value, 19, True, True,
id="unter Mindest-SoC, Speicher hat Vorrang -> Puffer leer"),
pytest.param(BatConsiderationMode.MIN_SOC_BAT.value, 20, True, False,
id="Mindest-SoC erreicht -> Puffer nicht leer"),
pytest.param(BatConsiderationMode.MIN_SOC_BAT.value, 19, False, False,
id="keine Entladeleistung freigegeben, es gibt keinen Puffer"),
pytest.param(BatConsiderationMode.BAT_MODE.value, 19, True, False,
id="Speicher-Modus, es gibt keinen Puffer"),
])
def test_is_buffer_depleted(mode: str, soc: int, power_discharge_active: bool, expected_result: bool):
# setup
b = BatAll()
b.data.config.configured = True
b.data.config.bat_control_activated = False
b.data.get.fault_state = 0
b.data.get.soc = soc
data.data.general_data.data.chargemode_config.bat.mode = mode
data.data.general_data.data.chargemode_config.bat.min_soc = 20
data.data.general_data.data.chargemode_config.bat.power_discharge_active = power_discharge_active

# execution
result = b.is_buffer_depleted()

# evaluation
assert result == expected_result
29 changes: 26 additions & 3 deletions packages/control/counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,26 @@ def calc_switch_off(self, load: Load) -> Tuple[float, float]:
f'freigegebener Überschuss {self.data.set.released_surplus}W')
return switch_off_power, threshold

BAT_BUFFERING = "Die Ladung wird gehalten, da der Speicher sie puffert."

def _bat_buffer_holds_charge(self, load: Load) -> bool:
""" prüft, ob der Speicher diese Ladung gegen die Abschaltschwelle stützt.

Die freigegebene Entladeleistung wird als Überschuss angeboten, damit eine laufende Ladung
weiterläuft. Bei mehrphasigen LP mit automatischer Umschaltung verhindert das bereits die
Rückschaltung auf eine Phase; bei ein- und festphasigen LP gibt es keine Umschaltung, die
greifen könnte, und die Ladung liefe ohne dieses Veto in die Abschaltverzögerung.

Nur für Ladepunkte mit tatsächlich fließender Ladung: der Puffer soll eine laufende Ladung
halten, aber keine neue starten. Verbraucher sind nicht gemeint, für sie gilt weiterhin die
Verbraucher-Abschaltschwelle.
"""
if isinstance(load, Chargepoint) is False:
return False
if bool(load.data.set.current_prev or load.data.get.charge_state) is False:
return False
return data.data.bat_all_data.is_buffering()

def switch_off_check_threshold(self, load: Load) -> bool:
""" prüft, ob die Abschaltschwelle erreicht wurde und startet die Abschaltverzögerung.
Ist die Abschaltverzögerung bereits aktiv, wird geprüft, ob die Abschaltschwelle wieder
Expand All @@ -537,6 +557,7 @@ def switch_off_check_threshold(self, load: Load) -> bool:
timestamp_switch_on_off = control_parameter.timestamp_switch_on_off

power_in_use, threshold = self.calc_switch_off(load)
bat_buffering = self._bat_buffer_holds_charge(load)
if control_parameter.state == ChargepointState.SWITCH_OFF_DELAY:
# Wenn automatische Phasenumschaltung aktiv, die Umschaltung abwarten, bevor die Abschaltschwelle
# greift.
Expand All @@ -547,10 +568,10 @@ def switch_off_check_threshold(self, load: Load) -> bool:
"Diese wird abgewartet, bevor die Abschaltverzögerung gestartet wird.")
# Wurde die Abschaltschwelle erreicht?
# Eigene Leistung aus der freigegebenen Leistung herausrechnen.
if power_in_use + load.data.set.required_power < threshold:
if bat_buffering or power_in_use + load.data.set.required_power < threshold:
timestamp_switch_on_off = None
self.data.set.released_surplus -= load.data.set.required_power
msg = texts.exceeded
msg = self.BAT_BUFFERING if bat_buffering else texts.exceeded
control_parameter.state = ChargepointState.CHARGING_ALLOWED
else:
# Wurde die Abschaltschwelle ggf. durch die Verzögerung anderer LP erreicht?
Expand All @@ -564,7 +585,9 @@ def switch_off_check_threshold(self, load: Load) -> bool:
self.data.set.reserved_surplus == 0))
if (switch_off_condition and (isinstance(load, Consumer) or
get_medium_charging_current(load.data.get.currents) <= min_current)):
if load.is_charging_stop_allowed():
if bat_buffering:
msg = self.BAT_BUFFERING
elif load.is_charging_stop_allowed():
# EV, die ohnehin nicht laden, wird direkt die Ladefreigabe entzogen.
# Würde man required_power vom released_evu_surplus subtrahieren, würden keine anderen EVs
# abgeschaltet werden und nach der Abschaltverzögerung des nicht ladenden EVs wäre die
Expand Down
59 changes: 59 additions & 0 deletions packages/control/counter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,62 @@ def test_reset_switch_on_off_releases_reserved_surplus_on_switch_on_delay(genera
assert evu_counter.data.set.reserved_surplus == 0
assert consumer.data.control_parameter.timestamp_switch_on_off is None
assert consumer.data.control_parameter.state == ChargepointState.NO_CHARGING_ALLOWED


@dataclass
class SwitchOffParams:
name: str
state: ChargepointState
charge_state: bool
is_buffering: bool
expected_state: ChargepointState
expected_msg: Optional[str]
expected_released_surplus: float


switch_off_cases = [
SwitchOffParams("laufende Abschaltverzögerung wird abgebrochen, wenn der Speicher puffert",
ChargepointState.SWITCH_OFF_DELAY, True, True,
ChargepointState.CHARGING_ALLOWED, Counter.BAT_BUFFERING, 0),
SwitchOffParams("ohne Puffer läuft die Abschaltverzögerung weiter",
ChargepointState.SWITCH_OFF_DELAY, True, False,
ChargepointState.SWITCH_OFF_DELAY, None, 1500),
SwitchOffParams("es wird keine Abschaltverzögerung gestartet, wenn der Speicher puffert",
ChargepointState.CHARGING_ALLOWED, True, True,
ChargepointState.CHARGING_ALLOWED, Counter.BAT_BUFFERING, 0),
SwitchOffParams("ohne Puffer wird die Abschaltverzögerung gestartet",
ChargepointState.CHARGING_ALLOWED, True, False,
ChargepointState.SWITCH_OFF_DELAY,
Counter.SWITCH_OFF_TEXTS_CP.waiting.format("1 Min."), 1500),
SwitchOffParams("eine nicht fließende Ladung wird nicht gehalten",
ChargepointState.CHARGING_ALLOWED, False, True,
ChargepointState.NO_CHARGING_ALLOWED, Counter.SWITCH_OFF_TEXTS_CP.not_charging, 0),
]


@pytest.mark.parametrize("params", switch_off_cases, ids=[c.name for c in switch_off_cases])
def test_switch_off_check_threshold_bat_buffering(params: SwitchOffParams, general_data_fixture, monkeypatch):
# setup
c = Counter(0)
c.data.set.released_surplus = 1500 if params.state == ChargepointState.SWITCH_OFF_DELAY else 0
cp = Chargepoint(0, None)
ev = Ev(0)
ev.data.charge_template = ChargeTemplate()
cp.data.set.charging_ev_data = ev
cp.data.set.required_power = 1500
cp.data.set.current_prev = 6 if params.charge_state else 0
cp.data.get.charge_state = params.charge_state
cp.data.get.currents = [6, 6, 6]
cp.data.control_parameter.state = params.state
cp.data.control_parameter.min_current = 6
# Abschaltschwelle überschritten: eigene Leistung herausgerechnet immer noch darüber
monkeypatch.setattr(Counter, "calc_switch_off", Mock(return_value=[2000, 0]))
monkeypatch.setattr(data.data.bat_all_data, "is_buffering", Mock(return_value=params.is_buffering))

# execution
c.switch_off_check_threshold(cp)

# evaluation
assert cp.data.control_parameter.state == params.expected_state
assert cp.data.get.state_str == params.expected_msg
assert c.data.set.released_surplus == params.expected_released_surplus
Loading
Loading