From 6b25cc27481666c2893cfc627d58b618fbf7f372 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Thu, 6 Aug 2026 11:53:05 +0200 Subject: [PATCH 1/8] Add Bidi to instant-charging --- packages/control/algorithm/bidi_charging.py | 53 +++++++++++++------ packages/control/algorithm/chargemodes.py | 48 +++++++++-------- packages/control/ev/charge_template.py | 13 ++++- packages/control/ev/ev.py | 3 +- packages/helpermodules/setdata.py | 2 +- packages/modules/common/store/_chargepoint.py | 15 ++++-- 6 files changed, 87 insertions(+), 47 deletions(-) diff --git a/packages/control/algorithm/bidi_charging.py b/packages/control/algorithm/bidi_charging.py index 4dec5a7b4c..d3439b5cdc 100644 --- a/packages/control/algorithm/bidi_charging.py +++ b/packages/control/algorithm/bidi_charging.py @@ -1,5 +1,6 @@ import logging from control import data +from control.chargemode import Chargemode from control.algorithm.chargemodes import CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE from control.algorithm.filter_chargepoints import get_chargepoints_with_required_current_by_chargemode from helpermodules.phase_handling import voltages_mean @@ -22,24 +23,42 @@ def set_bidi(self): f"Mode-Tuple {mode_tuple[0]} - {mode_tuple[1]} - {mode_tuple[2]}, Zähler {grid_counter.num}") while len(preferenced_cps): cp = preferenced_cps[0] - zero_point_adjustment = grid_counter.data.set.surplus_power_left / len(preferenced_cps) - log.debug(f"Nullpunktanpassung für LP{cp.num}: verbleibende Leistung {zero_point_adjustment}W") - missing_currents = [zero_point_adjustment / cp.data.get.phases_in_use / - 230 for i in range(0, cp.data.get.phases_in_use)] - missing_currents += [0] * (3 - len(missing_currents)) - if zero_point_adjustment > 0: - if cp.data.set.charging_ev_data.charge_template.bidi_charging_allowed( - cp.data.control_parameter.current_plan, cp.data.set.charging_ev_data.data.get.soc): - for index in range(0, 3): - missing_currents[index] = min(cp.data.control_parameter.required_current, - missing_currents[index]) - else: - log.info(f"LP{cp.num}: Nur bidirektional entladen erlaubt, da SoC-Limit erreicht.") - missing_currents = [0, 0, 0] + if preferenced_cps[0].data.control_parameter.chargemode == Chargemode.INSTANT_CHARGING: + + if cp.data.set.charging_ev_data.data.get.soc > 0: + # Auto-bat ist nicht leer + + dc_current = cp.data.set.charging_ev_data.charge_template.data.chargemode.instant_charging.dc_current + if dc_current < 0: + # Phasen in use berücksichtigen + missing_currents = [dc_current for i in range(0, cp.data.get.phases_in_use)] + missing_currents += [0] * (3 - len(missing_currents)) + + for index in range(0, 3): + missing_currents[index] = cp.check_min_max_current( + missing_currents[index], cp.data.get.phases_in_use) + else: - for index in range(0, 3): - missing_currents[index] = cp.check_min_max_current(missing_currents[index], - cp.data.get.phases_in_use) + # Default Nullpunktanpassung + zero_point_adjustment = grid_counter.data.set.surplus_power_left / len(preferenced_cps) + log.debug(f"Nullpunktanpassung für LP{cp.num}: verbleibende Leistung {zero_point_adjustment}W") + missing_currents = [zero_point_adjustment / cp.data.get.phases_in_use / + 230 for i in range(0, cp.data.get.phases_in_use)] + missing_currents += [0] * (3 - len(missing_currents)) + if zero_point_adjustment > 0: + if cp.data.set.charging_ev_data.charge_template.bidi_charging_allowed( + cp.data.control_parameter.current_plan, cp.data.set.charging_ev_data.data.get.soc): + for index in range(0, 3): + missing_currents[index] = min(cp.data.control_parameter.required_current, + missing_currents[index]) + else: + log.info(f"LP{cp.num}: Nur bidirektional entladen erlaubt, da SoC-Limit erreicht.") + missing_currents = [0, 0, 0] + else: + for index in range(0, 3): + missing_currents[index] = cp.check_min_max_current(missing_currents[index], + cp.data.get.phases_in_use) + grid_counter.update_surplus_values_left(missing_currents, voltages_mean(cp.data.get.voltages)) cp.data.set.current = missing_currents[0] log.info(f"LP{cp.num}: Stromstärke {missing_currents}A") diff --git a/packages/control/algorithm/chargemodes.py b/packages/control/algorithm/chargemodes.py index 0ebfa07cf3..0f74bfd56c 100644 --- a/packages/control/algorithm/chargemodes.py +++ b/packages/control/algorithm/chargemodes.py @@ -2,32 +2,34 @@ # Lademodi in absteigender Priorität # Tupel-Inhalt:(eingestellter Modus, tatsächlich genutzter Modus, Priorität) -CHARGEMODES = ((Chargemode.SCHEDULED_CHARGING, Chargemode.INSTANT_CHARGING, True), - (Chargemode.SCHEDULED_CHARGING, Chargemode.INSTANT_CHARGING, False), - (None, Chargemode.TIME_CHARGING, True), - (None, Chargemode.TIME_CHARGING, False), - (Chargemode.INSTANT_CHARGING, Chargemode.INSTANT_CHARGING, True), - (Chargemode.INSTANT_CHARGING, Chargemode.INSTANT_CHARGING, False), - (Chargemode.ECO_CHARGING, Chargemode.INSTANT_CHARGING, True), - (Chargemode.ECO_CHARGING, Chargemode.INSTANT_CHARGING, False), - (Chargemode.PV_CHARGING, Chargemode.INSTANT_CHARGING, True), - (Chargemode.PV_CHARGING, Chargemode.INSTANT_CHARGING, False), - (Chargemode.SCHEDULED_CHARGING, Chargemode.PV_CHARGING, True), - (Chargemode.SCHEDULED_CHARGING, Chargemode.PV_CHARGING, False), - (Chargemode.ECO_CHARGING, Chargemode.PV_CHARGING, True), - (Chargemode.ECO_CHARGING, Chargemode.PV_CHARGING, False), - (Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, True), - (Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, False), +CHARGEMODES = ((Chargemode.SCHEDULED_CHARGING, Chargemode.INSTANT_CHARGING, True), # 0 + (Chargemode.SCHEDULED_CHARGING, Chargemode.INSTANT_CHARGING, False), # 1 + (None, Chargemode.TIME_CHARGING, True), # 2 + (None, Chargemode.TIME_CHARGING, False), # 3 + (Chargemode.INSTANT_CHARGING, Chargemode.INSTANT_CHARGING, True), # 4 + (Chargemode.INSTANT_CHARGING, Chargemode.INSTANT_CHARGING, False), # 5 + (Chargemode.ECO_CHARGING, Chargemode.INSTANT_CHARGING, True), # 6 + (Chargemode.ECO_CHARGING, Chargemode.INSTANT_CHARGING, False), # 7 + (Chargemode.PV_CHARGING, Chargemode.INSTANT_CHARGING, True), # 8 + (Chargemode.PV_CHARGING, Chargemode.INSTANT_CHARGING, False), # 9 + (Chargemode.SCHEDULED_CHARGING, Chargemode.PV_CHARGING, True), # 10 + (Chargemode.SCHEDULED_CHARGING, Chargemode.PV_CHARGING, False), # 11 + (Chargemode.ECO_CHARGING, Chargemode.PV_CHARGING, True), # 12 + (Chargemode.ECO_CHARGING, Chargemode.PV_CHARGING, False), # 13 + (Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, True), # 14 + (Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, False), # 15 # niedrigere Priorität soll nachrangig geladen, aber zuerst entladen werden - (Chargemode.SCHEDULED_CHARGING, Chargemode.BIDI_CHARGING, False), - (Chargemode.SCHEDULED_CHARGING, Chargemode.BIDI_CHARGING, True), - (None, Chargemode.STOP, True), - (None, Chargemode.STOP, False)) + (Chargemode.SCHEDULED_CHARGING, Chargemode.BIDI_CHARGING, False), # 16 + (Chargemode.SCHEDULED_CHARGING, Chargemode.BIDI_CHARGING, True), # 17 + (Chargemode.INSTANT_CHARGING, Chargemode.BIDI_CHARGING, False), # 18 + (Chargemode.INSTANT_CHARGING, Chargemode.BIDI_CHARGING, True), # 19 + (None, Chargemode.STOP, True), # 20 + (None, Chargemode.STOP, False)) # 21 CONSIDERED_CHARGE_MODES_SURPLUS = CHARGEMODES[0:2] + CHARGEMODES[6:16] CONSIDERED_CHARGE_MODES_PV_ONLY = CHARGEMODES[10:16] CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT = CHARGEMODES[0:10] -CONSIDERED_CHARGE_MODES_MIN_CURRENT = CHARGEMODES[0:-4] -CONSIDERED_CHARGE_MODES_NO_CURRENT = CHARGEMODES[18:20] -CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE = CHARGEMODES[16:18] +CONSIDERED_CHARGE_MODES_MIN_CURRENT = CHARGEMODES[0:-6] +CONSIDERED_CHARGE_MODES_NO_CURRENT = CHARGEMODES[20:22] +CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE = CHARGEMODES[16:20] CONSIDERED_CHARGE_MODES_CHARGING = CHARGEMODES[0:16] diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 20f7ba88bb..748f973b30 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -197,11 +197,13 @@ def time_charging(self, SOC_REACHED = "Keine Ladung, da das Ladeziel bereits erreicht wurde." AMOUNT_REACHED = "Keine Ladung, da die Energiemenge bereits geladen wurde." + INSTANT_CHARGING_BIDI = "Fahrzeug entlädt." def instant_charging(self, soc: Optional[float], used_amount: float, - charging_type: str) -> Tuple[int, str, Optional[str], int]: + charging_type: str, + bidi_state: BidiState) -> Tuple[int, str, Optional[str], int]: """ prüft, ob die Lademengenbegrenzung erreicht wurde und setzt entsprechend den Ladestrom. """ message = None @@ -223,6 +225,15 @@ def instant_charging(self, current = 0 sub_mode = "stop" message = self.AMOUNT_REACHED + elif current < 0: + if bidi_state == BidiState.BIDI_CAPABLE: + sub_mode = "bidi_charging" + message = self.INSTANT_CHARGING_BIDI + else: + message = bidi_state.value + current = 0 + sub_mode = "stop" + return current, sub_mode, message, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.data.id)) diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index fab9167328..0ce96807b5 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -194,7 +194,8 @@ def get_required_current(self, required_current, submode, tmp_message, phases = charge_template.instant_charging( self.data.get.soc, imported_since_plugged, - charging_type) + charging_type, + bidi) elif charge_template.data.chargemode.selected == "pv_charging": required_current, submode, tmp_message, phases = charge_template.pv_charging( self.data.get.soc, control_parameter.min_current, charging_type, imported_since_plugged) diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 7973936181..9816e9dc9a 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -485,7 +485,7 @@ def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): else: self._validate_value(msg, float, [(float("-inf"), 0), (6, 32), (0, 0)]) elif "/set/required_power" in msg.topic: - self._validate_value(msg, float, [(0, float("inf"))]) + self._validate_value(msg, float, [(-11000.0, float("inf"))]) elif "/set/phases_to_use" in msg.topic: self._validate_value(msg, int, [(0, 3)]) elif ("/set/manual_lock" in msg.topic or diff --git a/packages/modules/common/store/_chargepoint.py b/packages/modules/common/store/_chargepoint.py index 292887f31b..728c44fdda 100644 --- a/packages/modules/common/store/_chargepoint.py +++ b/packages/modules/common/store/_chargepoint.py @@ -42,13 +42,20 @@ def update(self): pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/evse_current", self.state.evse_current) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/vehicle_id", self.state.vehicle_id) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/max_evse_current", self.state.max_evse_current) - pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/max_charge_power", self.state.max_charge_power) - pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/max_discharge_power", - self.state.max_discharge_power) + # ONLY_TEST Wieder rückgängig machen + pub_to_broker("openWB/set/chargepoint/" + str(self.num) + + "/get/max_charge_power", 500000 if self.state.max_charge_power is None else self.state.max_charge_power) + # ONLY_TEST Wieder rückgängig machen + pub_to_broker("openWB/set/chargepoint/" + str(self.num) + + "/get/max_discharge_power", -500000 if self.state.max_discharge_power is None else self.state.max_discharge_power) + pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/version", self.state.version) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/current_branch", self.state.current_branch) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/current_commit", self.state.current_commit) - pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/evse_signaling", self.state.evse_signaling) + + # ONLY_TEST Wieder rückgängig machen + pub_to_broker("openWB/set/chargepoint/" + str(self.num) + + "/get/evse_signaling", "HLC") # self.state.evse_signaling) # def get_chargepoint_value_store(id: int) -> ValueStore[ChargepointState]: From 02820e3300eb60c14fd8a3a6fcc32b052f849409 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Mon, 10 Aug 2026 12:48:34 +0200 Subject: [PATCH 2/8] Save Zwischenstand --- packages/control/algorithm/bidi_charging.py | 232 ++++++++++++++++---- packages/control/algorithm/chargemodes.py | 8 +- packages/control/counter.py | 79 ++++++- packages/control/loadmanagement.py | 78 +++++++ packages/helpermodules/setdata.py | 2 +- 5 files changed, 344 insertions(+), 55 deletions(-) diff --git a/packages/control/algorithm/bidi_charging.py b/packages/control/algorithm/bidi_charging.py index d3439b5cdc..4a03b89630 100644 --- a/packages/control/algorithm/bidi_charging.py +++ b/packages/control/algorithm/bidi_charging.py @@ -5,6 +5,17 @@ from control.algorithm.filter_chargepoints import get_chargepoints_with_required_current_by_chargemode from helpermodules.phase_handling import voltages_mean +from control.limiting_value import LoadmanagementLimit +from control.loadmanagement import Loadmanagement +from control.algorithm import common + +from control.chargepoint.chargepoint import Chargepoint +from typing import Iterable, List, Optional, Tuple + + +from helpermodules.phase_handling import voltages_mean + + log = logging.getLogger(__name__) @@ -21,45 +32,188 @@ def set_bidi(self): if preferenced_cps: log.info( f"Mode-Tuple {mode_tuple[0]} - {mode_tuple[1]} - {mode_tuple[2]}, Zähler {grid_counter.num}") + + # todo: counts richtig einbinden... + counts = [1, 1, 1] while len(preferenced_cps): cp = preferenced_cps[0] - if preferenced_cps[0].data.control_parameter.chargemode == Chargemode.INSTANT_CHARGING: - - if cp.data.set.charging_ev_data.data.get.soc > 0: - # Auto-bat ist nicht leer - - dc_current = cp.data.set.charging_ev_data.charge_template.data.chargemode.instant_charging.dc_current - if dc_current < 0: - # Phasen in use berücksichtigen - missing_currents = [dc_current for i in range(0, cp.data.get.phases_in_use)] - missing_currents += [0] * (3 - len(missing_currents)) - - for index in range(0, 3): - missing_currents[index] = cp.check_min_max_current( - missing_currents[index], cp.data.get.phases_in_use) - - else: - # Default Nullpunktanpassung - zero_point_adjustment = grid_counter.data.set.surplus_power_left / len(preferenced_cps) - log.debug(f"Nullpunktanpassung für LP{cp.num}: verbleibende Leistung {zero_point_adjustment}W") - missing_currents = [zero_point_adjustment / cp.data.get.phases_in_use / - 230 for i in range(0, cp.data.get.phases_in_use)] - missing_currents += [0] * (3 - len(missing_currents)) - if zero_point_adjustment > 0: - if cp.data.set.charging_ev_data.charge_template.bidi_charging_allowed( - cp.data.control_parameter.current_plan, cp.data.set.charging_ev_data.data.get.soc): - for index in range(0, 3): - missing_currents[index] = min(cp.data.control_parameter.required_current, - missing_currents[index]) - else: - log.info(f"LP{cp.num}: Nur bidirektional entladen erlaubt, da SoC-Limit erreicht.") - missing_currents = [0, 0, 0] - else: - for index in range(0, 3): - missing_currents[index] = cp.check_min_max_current(missing_currents[index], - cp.data.get.phases_in_use) - - grid_counter.update_surplus_values_left(missing_currents, voltages_mean(cp.data.get.voltages)) - cp.data.set.current = missing_currents[0] - log.info(f"LP{cp.num}: Stromstärke {missing_currents}A") + + cp.data.set.target_current = 0 + counters = data.data.counter_all_data.get_counters_to_check(cp.num) + + missing_currents = self.get_missing_currents(preferenced_cps, grid_counter) + log.debug(f"############_LP{cp.num}: missing currents {missing_currents}A") + + # Hier Limit per Loadmanagement + # -> geht alle Counter über dem cp durch und limit auf den niedrigsten erlaubten Wert + log.debug(f"ÜBERSCHUSS VOR COUNTER_LIMIT{grid_counter.data.set.surplus_power_left}") + for counter in counters: + available_currents, limit = Loadmanagement().get_available_currents_bidi( + missing_currents, voltages_mean(cp.data.get.voltages), data.data.counter_data[counter], cp) + + if limit.limiting_value is not None: + cp.data.control_parameter.limit = limit + + available_for_cp = self.available_current_for_cp( + cp, counts, available_currents, missing_currents) + + # Hier sagt man dann mann will nur das minimum setzen, + # -> wegen übergeordneten Zählern + current = self.get_current_to_set( + cp.data.set.current, available_for_cp, cp.data.set.target_current) + + log.debug(f"AV for CP {cp.num}: {available_for_cp} current {current}A") + + cp.data.set.current = current + log.info(f"LP{cp.num}: Stromstärke {current}A") + + log.debug(f"cp {cp.num} available currents {available_currents} missing currents " + f"{missing_currents} limit {limit.message} -----counter_{counter}" + f"CURRENT {current}A") + + log.debug(f"_After_check________LP{cp.num}: available currents {current}A") + + x = [current, current, current] + + grid_counter.update_surplus_values_left( + x, voltages_mean(cp.data.get.voltages)) + preferenced_cps.pop(0) + + def _set_loadmangement_message(self, + current: float, + limit: LoadmanagementLimit, + chargepoint: Chargepoint) -> None: + # Strom muss an diesem Zähler geändert werden + log.debug( + f"current {current} target {chargepoint.data.set.target_current} set current {chargepoint.data.set.current}" + f" required currents {chargepoint.data.control_parameter.required_currents}") + if (current != max(chargepoint.data.set.target_current, chargepoint.data.set.current or 0) and + # Strom erreicht nicht die vorgegebene Stromstärke + round(current, 2) != round(max( + chargepoint.data.control_parameter.required_currents), 2)): + chargepoint.set_state_and_log(f"Es kann nicht mit der vorgegebenen Stromstärke geladen werden" + f"{limit.message}") + + def get_counts(self, preferenced_chargepoints: List[Chargepoint]) -> List[int]: + + counts = [0]*3 + for chargepoint in preferenced_chargepoints: + required_currents = chargepoint.data.control_parameter.required_currents + for i in range(0, 3): + if required_currents[i] != 0: + counts[i] += 1 + return counts + + def get_missing_currents(self, preferenced_cps: List[Chargepoint], grid_counter) -> List[float]: + + cp = preferenced_cps[0] + missing_currents = [0, 0, 0] + if cp.data.control_parameter.chargemode == Chargemode.INSTANT_CHARGING: + log.debug(f"ÜBERSCHUSS_ALL für LP{cp.num}: {grid_counter.data.set.surplus_power_left }W") + if cp.data.set.charging_ev_data.data.get.soc > 0: + # Auto-bat ist nicht leer + + dc_current = cp.data.set.charging_ev_data.charge_template.data.chargemode.instant_charging.dc_current + if dc_current < 0: + # Phasen in use berücksichtigen + missing_currents = [dc_current for i in range(0, cp.data.get.phases_in_use)] + missing_currents += [0] * (3 - len(missing_currents)) + + for index in range(0, 3): + missing_currents[index] = cp.check_min_max_current( + missing_currents[index], cp.data.get.phases_in_use) + + else: + # Default Nullpunktanpassung + zero_point_adjustment = grid_counter.data.set.surplus_power_left / len(preferenced_cps) + log.debug(f"ÜBERSCHUSS_ALL für LP{cp.num}: {grid_counter.data.set.surplus_power_left }W") + log.debug(f"Nullpunktanpassung für LP{cp.num}: verbleibende Leistung {zero_point_adjustment}W") + missing_currents = [zero_point_adjustment / cp.data.get.phases_in_use / + 230 for i in range(0, cp.data.get.phases_in_use)] + missing_currents += [0] * (3 - len(missing_currents)) + if zero_point_adjustment > 0: + if cp.data.set.charging_ev_data.charge_template.bidi_charging_allowed( + cp.data.control_parameter.current_plan, cp.data.set.charging_ev_data.data.get.soc): + for index in range(0, 3): + missing_currents[index] = min(cp.data.control_parameter.required_current, + missing_currents[index]) + else: + log.info(f"LP{cp.num}: Nur bidirektional entladen erlaubt, da SoC-Limit erreicht.") + missing_currents = [0, 0, 0] + else: + for index in range(0, 3): + missing_currents[index] = cp.check_min_max_current(missing_currents[index], + cp.data.get.phases_in_use) + + return missing_currents + + def get_current_to_set(self, set_current: float, diff: float, prev_current: float) -> float: + """Der neue Strom darf nicht höher als der in dieser Stufe bisher gesetzter sein, um das LM der untergeordneten + Zähler nicht zu untergraben. Der Vergleich muss positiv sein, wenn zum ersten Mal auf dieser Stufe ein Strom gesetzt + wird.""" + new_current = prev_current + diff + if set_current is not None: + if diff < 0: + if new_current < set_current: + log.debug("Neuer Soll-Strom darf beim Entladen nicht niedriger als bisher gesetzter sein: " + f"bisher {set_current}A, neuer {new_current}") + return set_current + else: + if new_current > set_current: + log.debug("BIDI_Neuer Soll-Strom darf nicht höher als bisher gesetzter sein: " + f"bisher {set_current}A, neuer {new_current}") + return set_current + return new_current + + def available_current_for_cp(self, chargepoint: Chargepoint, + counts: List[int], + available_currents: List[float], + missing_currents: List[float]) -> float: + control_parameter = chargepoint.data.control_parameter + missing_current_cp = control_parameter.required_current - chargepoint.data.set.target_current + is_discharge = missing_current_cp < 0 + available_current = float("-inf") if is_discharge else float("inf") + for i in range(0, 3): + if control_parameter.required_currents[i] == 0: + continue + if counts[i] <= 0: + continue + + phase_available_current = available_currents[i] / counts[i] + if is_discharge: + available_current = max( + max(missing_current_cp, phase_available_current), available_current) + else: + available_current = min( + min(missing_current_cp, phase_available_current), available_current) + if available_current in [float("inf"), float("-inf")]: + available_current = missing_current_cp + return available_current + + def set_current_counterdiff(self, + diff_current: float, + current: float, + chargepoint: Chargepoint, + surplus: bool = False) -> None: + required_currents = chargepoint.data.control_parameter.required_currents + # considered_current = consider_less_charging_chargepoint_in_loadmanagement( + # chargepoint, current) + # gar nicht ladende Autos? + diff = diff_current + diffs = [diff if required_currents[i] != 0 else 0 for i in range(3)] + if max(diffs) > 0: + counters = data.data.counter_all_data.get_counters_to_check(chargepoint.num) + for counter in counters: + if surplus: + data.data.counter_data[counter].update_surplus_values_left( + diffs, + voltages_mean(chargepoint.data.get.voltages)) + else: + data.data.counter_data[counter].update_values_left( + diffs, + voltages_mean(chargepoint.data.get.voltages)) + data.data.io_actions.dimming_set_import_power_left({"type": "cp", "id": chargepoint.num}, sum(diffs)*230) + + chargepoint.data.set.current = current + log.info(f"LP{chargepoint.num}: Stromstärke {current}A") diff --git a/packages/control/algorithm/chargemodes.py b/packages/control/algorithm/chargemodes.py index 0f74bfd56c..8c033a5709 100644 --- a/packages/control/algorithm/chargemodes.py +++ b/packages/control/algorithm/chargemodes.py @@ -19,17 +19,17 @@ (Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, True), # 14 (Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, False), # 15 # niedrigere Priorität soll nachrangig geladen, aber zuerst entladen werden - (Chargemode.SCHEDULED_CHARGING, Chargemode.BIDI_CHARGING, False), # 16 - (Chargemode.SCHEDULED_CHARGING, Chargemode.BIDI_CHARGING, True), # 17 (Chargemode.INSTANT_CHARGING, Chargemode.BIDI_CHARGING, False), # 18 (Chargemode.INSTANT_CHARGING, Chargemode.BIDI_CHARGING, True), # 19 + (Chargemode.SCHEDULED_CHARGING, Chargemode.BIDI_CHARGING, False), # 16 + (Chargemode.SCHEDULED_CHARGING, Chargemode.BIDI_CHARGING, True), # 17 (None, Chargemode.STOP, True), # 20 (None, Chargemode.STOP, False)) # 21 CONSIDERED_CHARGE_MODES_SURPLUS = CHARGEMODES[0:2] + CHARGEMODES[6:16] CONSIDERED_CHARGE_MODES_PV_ONLY = CHARGEMODES[10:16] -CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT = CHARGEMODES[0:10] -CONSIDERED_CHARGE_MODES_MIN_CURRENT = CHARGEMODES[0:-6] +CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT = CHARGEMODES[0:10] # + CHARGEMODES[16:20] +CONSIDERED_CHARGE_MODES_MIN_CURRENT = CHARGEMODES[0:-6] # + CHARGEMODES[16:18] CONSIDERED_CHARGE_MODES_NO_CURRENT = CHARGEMODES[20:22] CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE = CHARGEMODES[16:20] CONSIDERED_CHARGE_MODES_CHARGING = CHARGEMODES[0:16] diff --git a/packages/control/counter.py b/packages/control/counter.py index e7351b3cef..2b04e450bb 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -80,6 +80,11 @@ class Set: released_surplus: float = field(default=0, metadata={"topic": "set/released_surplus"}) raw_power_left: Optional[float] = 0 raw_currents_left: List[float] = field(default_factory=currents_list_factory) + + # Bidi Entladung + raw_exported_power_left: Optional[float] = 0 + raw_exported_currents_left: List[float] = field(default_factory=currents_list_factory) + surplus_power_left: float = 0 @@ -155,17 +160,32 @@ def _set_current_left(self, loadmanagement_available: bool) -> None: except KeyError: element_current = [get_medium_charging_current(chargepoint.data.get.currents)]*3 currents_raw = list(map(operator.sub, currents_raw, element_current)) - currents_raw = list(map(operator.sub, self.data.config.max_currents, currents_raw)) - if min(currents_raw) < 0: - log.debug(f"Verbleibende Ströme: {currents_raw}, Überbelastung wird durch Hausverbrauch verursacht") - currents_raw = [max(currents_raw[i], 0) for i in range(0, 3)] - self.data.set.raw_currents_left = currents_raw + # verbleibende Ströme für Laden (+) und Entladen (-) getrennt vorhalten + raw_currents_left = list(map(operator.sub, self.data.config.max_currents, currents_raw)) + raw_exported_currents_left = list(map(operator.add, self.data.config.max_currents, currents_raw)) + + if min(raw_currents_left) < 0: + log.debug( + f"Verbleibende Ströme Laden: {raw_currents_left}, Überbelastung wird durch Hausverbrauch verursacht") + raw_currents_left = [max(raw_currents_left[i], 0) for i in range(0, 3)] + + if min(raw_exported_currents_left) < 0: + log.debug(("Verbleibende Ströme Entladen: " + f"{raw_exported_currents_left}, Überbelastung wird durch Einspeisung verursacht")) + raw_exported_currents_left = [max(raw_exported_currents_left[i], 0) for i in range(0, 3)] + + self.data.set.raw_currents_left = raw_currents_left + self.data.set.raw_exported_currents_left = raw_exported_currents_left log.info(f'Verbleibende Ströme an Zähler {self.num}: {self.data.set.raw_currents_left}A') + log.info((f'Verbleibende exportierte Ströme an Zähler {self.num}: ' + f'{self.data.set.raw_exported_currents_left}A')) else: self.data.set.raw_currents_left = [self.data.config.max_power_errorcase/230/3]*3 + self.data.set.raw_exported_currents_left = [self.data.config.max_power_errorcase/230/3]*3 log.info(f'Verbleibende Ströme an Zähler {self.num} (Fehlerfall): {self.data.set.raw_currents_left}A') # tested + def get_unbalanced_load_exceeding(self, raw_currents_left: List[float]) -> List[float]: """gibt eine Liste zurück, die für jede Phase angibt, um wie viel Ampere die Schieflast überschritten wurde. So können gezielt Fahrzeuge reduziert werden, die auf dieser/n Phase(n) laden. Die Phase mit dem höchsten @@ -187,29 +207,66 @@ def _set_power_left(self, loadmanagement_available: bool) -> None: for cp in data.data.cp_data.values(): power_raw -= cp.data.get.power self.data.set.raw_power_left = self.data.config.max_total_power - power_raw + self.data.set.raw_exported_power_left = self.data.config.max_total_power + power_raw log.info(f'Verbleibende Leistung an Zähler {self.num}: {self.data.set.raw_power_left}W') + log.info((f'Verbleibende exportierte Leistung an Zähler {self.num}: ' + f'{self.data.set.raw_exported_power_left}W')) else: self.data.set.raw_power_left = self.data.config.max_power_errorcase + self.data.set.raw_exported_power_left = self.data.config.max_power_errorcase log.info(f'Verbleibende Leistung an Zähler {self.num} (Fehlerfall): {self.data.set.raw_power_left}W') + log.info((f'Verbleibende exportierte Leistung an Zähler {self.num} (Fehlerfall): ' + f'{self.data.set.raw_exported_power_left}W')) else: self.data.set.raw_power_left = None + self.data.set.raw_exported_power_left = None def update_values_left(self, diffs, cp_voltage: float) -> None: # Mittelwert der Spannungen verwenden, um Phasenverdrehung zu kompensieren # (Probleme bei einphasig angeschlossenen Wallboxen) - self.data.set.raw_currents_left = list(map(operator.sub, self.data.set.raw_currents_left, diffs)) - if self.data.set.raw_power_left is not None: - self.data.set.raw_power_left -= sum([c * cp_voltage for c in diffs]) - log.debug(f'Zähler {self.num}: {self.data.set.raw_currents_left}A verbleibende Ströme, ' - f'{self.data.set.raw_power_left}W verbleibende Leistung') + + self._update_raw_values(diffs, cp_voltage) + # self.data.set.raw_currents_left = list(map(operator.sub, self.data.set.raw_currents_left, diffs)) + # self.data.set.raw_exported_currents_left = list( + # map(operator.add, self.data.set.raw_exported_currents_left, diffs)) + + # if self.data.set.raw_power_left is not None: + # self.data.set.raw_power_left -= sum([c * cp_voltage for c in diffs]) + # if self.data.set.raw_exported_power_left is not None: + # self.data.set.raw_exported_power_left += sum([c * cp_voltage for c in diffs]) + + # log.debug(f'Zähler {self.num}: {self.data.set.raw_currents_left}A verbleibende Ströme, ' + # f'{self.data.set.raw_exported_currents_left}A verbleibende exportierte Ströme', + # f'{self.data.set.raw_power_left}W verbleibende Leistung', + # f'{self.data.set.raw_exported_power_left}W verbleibende exportierte Leistung') def update_surplus_values_left(self, diffs, cp_voltage: float) -> None: - self.data.set.raw_currents_left = list(map(operator.sub, self.data.set.raw_currents_left, diffs)) + self._update_raw_values(diffs, cp_voltage) + if self.data.set.surplus_power_left is not None: + log.debug(f"VORANPASSUNG_____{self.data.set.surplus_power_left}") self.data.set.surplus_power_left -= sum([c * cp_voltage for c in diffs]) + log.debug(f"VORANPASSUNG_____{self.data.set.surplus_power_left}") + log.debug(f'Zähler {self.num}: {self.data.set.raw_currents_left}A verbleibende Ströme, ' f'{self.data.set.surplus_power_left}W verbleibender Überschuss') + def _update_raw_values(self, diffs, cp_voltage: float) -> None: + + self.data.set.raw_currents_left = list(map(operator.sub, self.data.set.raw_currents_left, diffs)) + self.data.set.raw_exported_currents_left = list( + map(operator.add, self.data.set.raw_exported_currents_left, diffs)) + + if self.data.set.raw_power_left is not None: + self.data.set.raw_power_left -= sum([c * cp_voltage for c in diffs]) + if self.data.set.raw_exported_power_left is not None: + self.data.set.raw_exported_power_left += sum([c * cp_voltage for c in diffs]) + + log.debug(f'Zähler {self.num}: {self.data.set.raw_currents_left}A verbleibende Ströme, ' + f'{self.data.set.raw_exported_currents_left}A verbleibende exportierte Ströme, ' + f'{self.data.set.raw_power_left}W verbleibende Leistung, ' + f'{self.data.set.raw_exported_power_left}W verbleibende exportierte Leistung') + def calc_surplus(self): # reservierte Leistung wird nicht berücksichtigt, weil diese noch verwendet werden kann, bis die EV # eingeschaltet werden. Es darf bloß nicht für zu viele zB die Einschaltverzögerung gestartet werden. diff --git a/packages/control/loadmanagement.py b/packages/control/loadmanagement.py index ae12e06f3c..4f5dd93a28 100644 --- a/packages/control/loadmanagement.py +++ b/packages/control/loadmanagement.py @@ -72,6 +72,84 @@ def get_available_currents_surplus(self, limit = new_limit if new_limit.limiting_value is not None else limit return available_currents, limit + def get_available_currents_bidi(self, + missing_currents: List[float], + cp_voltage: float, + counter: Counter, + cp: Chargepoint, + feed_in: int = 0) -> Tuple[List[float], LoadmanagementLimit]: + raw_currents_left = counter.data.set.raw_currents_left + raw_exported_currents_left = counter.data.set.raw_exported_currents_left + + available_currents, limit = self._limit_by_current_bidi( + counter, missing_currents, raw_currents_left, raw_exported_currents_left) + + available_currents, new_limit = self._limit_by_power_bidi( + counter, + available_currents, + cp_voltage, + counter.data.set.raw_power_left, + counter.data.set.raw_exported_power_left, + feed_in) + limit = new_limit if new_limit.limiting_value is not None else limit + + return available_currents, limit + + def _limit_by_power_bidi(self, + counter: Counter, + available_currents: List[float], + cp_voltage: float, + raw_power_left: Optional[float], + raw_exported_power_left: Optional[float], + feed_in: Optional[float]) -> Tuple[List[float], LoadmanagementLimit]: + # Mittelwert der Spannungen verwenden, um Phasenverdrehung zu kompensieren + # (Probleme bei einphasig angeschlossenen Wallboxen) + currents = available_currents.copy() + limit = LoadmanagementLimit(None, None) + total_power = sum([c * cp_voltage for c in available_currents]) + power_left = raw_power_left if total_power >= 0 else raw_exported_power_left + + if power_left is None: + return currents, limit + elif power_left > 0: + if feed_in is not None: + power_left = max(power_left - feed_in, 0) + log.debug(f"Verbleibende Leistung unter Berücksichtigung der Einspeisegrenze: {power_left}W") + if abs(total_power) > power_left: + for i in range(0, 3): + try: + # Am meisten belastete Phase trägt am meisten zur Leistungsreduktion bei. + currents[i] = available_currents[i] / sum(available_currents) * power_left / cp_voltage + except ZeroDivisionError: + # bei einphasig angeschlossenen Wallboxen ist die Spannung der anderen Phasen 0V + currents[i] = 0.0 + log.debug(f"Leistungsüberschreitung auf {power_left}W korrigieren: {available_currents}") + limit = LoadmanagementLimit(LimitingValue.POWER.value.format(get_component_name_by_id(counter.num)), + LimitingValue.POWER) + return currents, limit + else: + return [0]*3, LoadmanagementLimit(LimitingValue.POWER.value.format(get_component_name_by_id(counter.num)), + LimitingValue.POWER) + + # tested + def _limit_by_current_bidi(self, + counter: Counter, + missing_currents: List[float], + raw_currents_left: List[float], + raw_exported_currents_left: List[float]) -> Tuple[List[float], LoadmanagementLimit]: + available_currents = [0.0]*3 + limit = LoadmanagementLimit(None, None) + for i in range(0, 3): + if missing_currents[i] >= 0: + available_currents[i] = min(missing_currents[i], raw_currents_left[i]) + else: + available_currents[i] = max(missing_currents[i], -raw_exported_currents_left[i]) + if available_currents != missing_currents: + log.debug(f"Stromüberschreitung {missing_currents}W korrigieren: {available_currents}") + limit = LoadmanagementLimit(LimitingValue.CURRENT.value.format(get_component_name_by_id(counter.num)), + LimitingValue.CURRENT) + return available_currents, limit + def _limit_by_unbalanced_load(self, counter: Counter, available_currents: List[float], diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 9816e9dc9a..764bfa35df 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -485,7 +485,7 @@ def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): else: self._validate_value(msg, float, [(float("-inf"), 0), (6, 32), (0, 0)]) elif "/set/required_power" in msg.topic: - self._validate_value(msg, float, [(-11000.0, float("inf"))]) + self._validate_value(msg, float) elif "/set/phases_to_use" in msg.topic: self._validate_value(msg, int, [(0, 3)]) elif ("/set/manual_lock" in msg.topic or From bddd3c700ccd13f938b09b1ae8516e3ab4ab49e5 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Tue, 11 Aug 2026 09:44:05 +0200 Subject: [PATCH 3/8] Fix counts in bidi --- packages/control/algorithm/bidi_charging.py | 98 +++++++-------------- 1 file changed, 31 insertions(+), 67 deletions(-) diff --git a/packages/control/algorithm/bidi_charging.py b/packages/control/algorithm/bidi_charging.py index 4a03b89630..09b8cf0a94 100644 --- a/packages/control/algorithm/bidi_charging.py +++ b/packages/control/algorithm/bidi_charging.py @@ -7,13 +7,9 @@ from control.limiting_value import LoadmanagementLimit from control.loadmanagement import Loadmanagement -from control.algorithm import common from control.chargepoint.chargepoint import Chargepoint -from typing import Iterable, List, Optional, Tuple - - -from helpermodules.phase_handling import voltages_mean +from typing import List log = logging.getLogger(__name__) @@ -26,43 +22,38 @@ def __init__(self): def set_bidi(self): grid_counter = data.data.counter_all_data.get_evu_counter() log.debug(f"Nullpunktanpassung {grid_counter.data.set.surplus_power_left}W") - zero_point_adjustment = grid_counter for mode_tuple in CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE: preferenced_cps = get_chargepoints_with_required_current_by_chargemode(mode_tuple) if preferenced_cps: log.info( f"Mode-Tuple {mode_tuple[0]} - {mode_tuple[1]} - {mode_tuple[2]}, Zähler {grid_counter.num}") - # todo: counts richtig einbinden... - counts = [1, 1, 1] while len(preferenced_cps): cp = preferenced_cps[0] + counts = self.get_counts(cp) cp.data.set.target_current = 0 - counters = data.data.counter_all_data.get_counters_to_check(cp.num) missing_currents = self.get_missing_currents(preferenced_cps, grid_counter) - log.debug(f"############_LP{cp.num}: missing currents {missing_currents}A") + log.debug(f"Bidi-LP{cp.num}: missing currents {missing_currents}A") - # Hier Limit per Loadmanagement - # -> geht alle Counter über dem cp durch und limit auf den niedrigsten erlaubten Wert - log.debug(f"ÜBERSCHUSS VOR COUNTER_LIMIT{grid_counter.data.set.surplus_power_left}") + counters = data.data.counter_all_data.get_counters_to_check(cp.num) for counter in counters: available_currents, limit = Loadmanagement().get_available_currents_bidi( - missing_currents, voltages_mean(cp.data.get.voltages), data.data.counter_data[counter], cp) + missing_currents, voltages_mean(cp.data.get.voltages), data.data.counter_data[counter]) if limit.limiting_value is not None: cp.data.control_parameter.limit = limit - available_for_cp = self.available_current_for_cp( + available_for_cp = self.available_current_for_bidi_cp( cp, counts, available_currents, missing_currents) - # Hier sagt man dann mann will nur das minimum setzen, - # -> wegen übergeordneten Zählern + # Der neue Strom darf nicht höher als der in dieser Stufe bisher gesetzter sein current = self.get_current_to_set( cp.data.set.current, available_for_cp, cp.data.set.target_current) - log.debug(f"AV for CP {cp.num}: {available_for_cp} current {current}A") + # Ausgabe LIMIT-MSG + self._set_loadmangement_message(current, limit, cp) cp.data.set.current = current log.info(f"LP{cp.num}: Stromstärke {current}A") @@ -88,25 +79,27 @@ def _set_loadmangement_message(self, log.debug( f"current {current} target {chargepoint.data.set.target_current} set current {chargepoint.data.set.current}" f" required currents {chargepoint.data.control_parameter.required_currents}") - if (current != max(chargepoint.data.set.target_current, chargepoint.data.set.current or 0) and + if (limit.message and current != max(chargepoint.data.set.target_current, chargepoint.data.set.current or 0) and # Strom erreicht nicht die vorgegebene Stromstärke round(current, 2) != round(max( chargepoint.data.control_parameter.required_currents), 2)): - chargepoint.set_state_and_log(f"Es kann nicht mit der vorgegebenen Stromstärke geladen werden" - f"{limit.message}") + if current < 0: + chargepoint.set_state_and_log(f"MY_Es kann nicht mit der vorgegebenen Stromstärke entladen werden" + f"{limit.message}") + else: + chargepoint.set_state_and_log(f"MY_Es kann nicht mit der vorgegebenen Stromstärke geladen werden" + f"{limit.message}") - def get_counts(self, preferenced_chargepoints: List[Chargepoint]) -> List[int]: + def get_counts(self, chargepoint: Chargepoint) -> List[int]: counts = [0]*3 - for chargepoint in preferenced_chargepoints: - required_currents = chargepoint.data.control_parameter.required_currents - for i in range(0, 3): - if required_currents[i] != 0: - counts[i] += 1 + required_currents = chargepoint.data.control_parameter.required_currents + for i in range(3): + if required_currents[i] != 0: + counts[i] += 1 return counts def get_missing_currents(self, preferenced_cps: List[Chargepoint], grid_counter) -> List[float]: - cp = preferenced_cps[0] missing_currents = [0, 0, 0] if cp.data.control_parameter.chargemode == Chargemode.INSTANT_CHARGING: @@ -149,9 +142,9 @@ def get_missing_currents(self, preferenced_cps: List[Chargepoint], grid_counter) return missing_currents def get_current_to_set(self, set_current: float, diff: float, prev_current: float) -> float: - """Der neue Strom darf nicht höher als der in dieser Stufe bisher gesetzter sein, um das LM der untergeordneten - Zähler nicht zu untergraben. Der Vergleich muss positiv sein, wenn zum ersten Mal auf dieser Stufe ein Strom gesetzt - wird.""" + """Der neue Strom darf nicht höher als der in dieser Stufe bisher gesetzter sein, + um das LM der untergeordneten Zähler nicht zu untergraben. Der Vergleich muss positiv + sein, wenn zum ersten Mal auf dieser Stufe ein Strom gesetzt wird.""" new_current = prev_current + diff if set_current is not None: if diff < 0: @@ -166,20 +159,18 @@ def get_current_to_set(self, set_current: float, diff: float, prev_current: floa return set_current return new_current - def available_current_for_cp(self, chargepoint: Chargepoint, - counts: List[int], - available_currents: List[float], - missing_currents: List[float]) -> float: + def available_current_for_bidi_cp(self, chargepoint: Chargepoint, + counts: List[int], + available_currents: List[float], + missing_currents: List[float]) -> float: + + # control_parameter.required_current + # -> ist immer positve aktuell und gibt die maximal benötigte Stromstärke an control_parameter = chargepoint.data.control_parameter missing_current_cp = control_parameter.required_current - chargepoint.data.set.target_current is_discharge = missing_current_cp < 0 available_current = float("-inf") if is_discharge else float("inf") for i in range(0, 3): - if control_parameter.required_currents[i] == 0: - continue - if counts[i] <= 0: - continue - phase_available_current = available_currents[i] / counts[i] if is_discharge: available_current = max( @@ -190,30 +181,3 @@ def available_current_for_cp(self, chargepoint: Chargepoint, if available_current in [float("inf"), float("-inf")]: available_current = missing_current_cp return available_current - - def set_current_counterdiff(self, - diff_current: float, - current: float, - chargepoint: Chargepoint, - surplus: bool = False) -> None: - required_currents = chargepoint.data.control_parameter.required_currents - # considered_current = consider_less_charging_chargepoint_in_loadmanagement( - # chargepoint, current) - # gar nicht ladende Autos? - diff = diff_current - diffs = [diff if required_currents[i] != 0 else 0 for i in range(3)] - if max(diffs) > 0: - counters = data.data.counter_all_data.get_counters_to_check(chargepoint.num) - for counter in counters: - if surplus: - data.data.counter_data[counter].update_surplus_values_left( - diffs, - voltages_mean(chargepoint.data.get.voltages)) - else: - data.data.counter_data[counter].update_values_left( - diffs, - voltages_mean(chargepoint.data.get.voltages)) - data.data.io_actions.dimming_set_import_power_left({"type": "cp", "id": chargepoint.num}, sum(diffs)*230) - - chargepoint.data.set.current = current - log.info(f"LP{chargepoint.num}: Stromstärke {current}A") From 04c0a5f8e7a295297a4ea7eebf4bf1e1de9625ba Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Tue, 11 Aug 2026 12:53:07 +0200 Subject: [PATCH 4/8] Fix Counter --- packages/control/counter.py | 48 ++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/control/counter.py b/packages/control/counter.py index 2b04e450bb..4a089f7c9f 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -149,7 +149,10 @@ def _get_loadmanagement_state(self) -> None: def _set_current_left(self, loadmanagement_available: bool) -> None: if loadmanagement_available: + if self.num == 26: + log.debug("-.--.-.-.-") currents_raw = self.data.get.currents + currents_exported_raw = self.data.get.currents cp_keys = data.data.counter_all_data.get_chargepoints_of_counter(f"counter{self.num}") for cp_key in cp_keys: chargepoint = data.data.cp_data[cp_key] @@ -159,14 +162,35 @@ def _set_current_left(self, loadmanagement_available: bool) -> None: chargepoint.data.get.currents) except KeyError: element_current = [get_medium_charging_current(chargepoint.data.get.currents)]*3 + + # Ich schau, was liegt aktuell am Zähler an -> currents_raw + # Dann ziehe ich davon alle cps ab -> currents_raw = list(map(operator.sub, currents_raw, element_current)) + # => dann habe ich in currents_raw nur noch die Ströme die nicht zum laden verwendet werden + if min(element_current) < 0: + # currents_exported_raw = alles ohne entladen -> sprich nur die Hausverbraucher ohne Einspeisung + currents_exported_raw = list(map(operator.sub, currents_exported_raw, element_current)) + continue + # currents_raw = alles ohne laden -> sprich nur die Hausverbraucher und Einspeisung currents_raw = list(map(operator.sub, currents_raw, element_current)) - # verbleibende Ströme für Laden (+) und Entladen (-) getrennt vorhalten + + # Diese ziehe ich dann von den max_wert des Counters ab + # -> Bekomme die verbleibenden Ströme die zum Laden verwendet werden können + + # Das Entladen steckt ja schon in currents_raw drin -> direkter Zählerwert + # Dadurch das nur die wirklich ladenen Cps rausrechnen werden -> bleibt Hausverbrauch und Entladen übrig + # Dann max-Wert vom Counter minus Hausverbrauch und Entladen -> Verbleibende Ströme die zum Laden verwendet werden können + # Wenn mehr entladen als Hausverbrauch -> dann ist der Wert negativ + # Und das limit am Counter erhöht sich -> mehr Strom zum Laden verfügbar + raw_currents_left = list(map(operator.sub, self.data.config.max_currents, currents_raw)) - raw_exported_currents_left = list(map(operator.add, self.data.config.max_currents, currents_raw)) + + # Addiere alle Verbraucher dazu, weil wir das ja mehr entladen können dann + raw_exported_currents_left = list(map(operator.add, self.data.config.max_currents, currents_exported_raw)) if min(raw_currents_left) < 0: log.debug( - f"Verbleibende Ströme Laden: {raw_currents_left}, Überbelastung wird durch Hausverbrauch verursacht") + f"Verbleibende Ströme Laden: {raw_currents_left}, " + f"Überbelastung wird durch Hausverbrauch verursacht") raw_currents_left = [max(raw_currents_left[i], 0) for i in range(0, 3)] if min(raw_exported_currents_left) < 0: @@ -205,6 +229,8 @@ def _set_power_left(self, loadmanagement_available: bool) -> None: if loadmanagement_available: power_raw = self.data.get.power for cp in data.data.cp_data.values(): + if cp.data.get.power < 0: + continue power_raw -= cp.data.get.power self.data.set.raw_power_left = self.data.config.max_total_power - power_raw self.data.set.raw_exported_power_left = self.data.config.max_total_power + power_raw @@ -224,29 +250,13 @@ def _set_power_left(self, loadmanagement_available: bool) -> None: def update_values_left(self, diffs, cp_voltage: float) -> None: # Mittelwert der Spannungen verwenden, um Phasenverdrehung zu kompensieren # (Probleme bei einphasig angeschlossenen Wallboxen) - self._update_raw_values(diffs, cp_voltage) - # self.data.set.raw_currents_left = list(map(operator.sub, self.data.set.raw_currents_left, diffs)) - # self.data.set.raw_exported_currents_left = list( - # map(operator.add, self.data.set.raw_exported_currents_left, diffs)) - - # if self.data.set.raw_power_left is not None: - # self.data.set.raw_power_left -= sum([c * cp_voltage for c in diffs]) - # if self.data.set.raw_exported_power_left is not None: - # self.data.set.raw_exported_power_left += sum([c * cp_voltage for c in diffs]) - - # log.debug(f'Zähler {self.num}: {self.data.set.raw_currents_left}A verbleibende Ströme, ' - # f'{self.data.set.raw_exported_currents_left}A verbleibende exportierte Ströme', - # f'{self.data.set.raw_power_left}W verbleibende Leistung', - # f'{self.data.set.raw_exported_power_left}W verbleibende exportierte Leistung') def update_surplus_values_left(self, diffs, cp_voltage: float) -> None: self._update_raw_values(diffs, cp_voltage) if self.data.set.surplus_power_left is not None: - log.debug(f"VORANPASSUNG_____{self.data.set.surplus_power_left}") self.data.set.surplus_power_left -= sum([c * cp_voltage for c in diffs]) - log.debug(f"VORANPASSUNG_____{self.data.set.surplus_power_left}") log.debug(f'Zähler {self.num}: {self.data.set.raw_currents_left}A verbleibende Ströme, ' f'{self.data.set.surplus_power_left}W verbleibender Überschuss') From 5d5463de339ae94cb3bf2b76de8b85c4eef8a311 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Wed, 12 Aug 2026 09:22:50 +0200 Subject: [PATCH 5/8] Refactor and Improve Bidi charging logic --- packages/control/algorithm/bidi_charging.py | 68 +++---------- packages/control/algorithm/chargemodes.py | 12 +-- packages/control/algorithm/common.py | 37 +++++-- .../integration_test/bidi_charging_test.py | 12 ++- packages/control/counter.py | 68 +++++-------- packages/control/ev/charge_template_test.py | 2 +- packages/control/loadmanagement.py | 98 +++++-------------- packages/modules/common/store/_chargepoint.py | 6 +- 8 files changed, 114 insertions(+), 189 deletions(-) diff --git a/packages/control/algorithm/bidi_charging.py b/packages/control/algorithm/bidi_charging.py index 09b8cf0a94..277d810640 100644 --- a/packages/control/algorithm/bidi_charging.py +++ b/packages/control/algorithm/bidi_charging.py @@ -9,6 +9,7 @@ from control.loadmanagement import Loadmanagement from control.chargepoint.chargepoint import Chargepoint +import control.algorithm.common as common from typing import List @@ -45,11 +46,11 @@ def set_bidi(self): if limit.limiting_value is not None: cp.data.control_parameter.limit = limit - available_for_cp = self.available_current_for_bidi_cp( - cp, counts, available_currents, missing_currents) + available_for_cp = common.available_current_for_cp( + cp, counts, available_currents, missing_currents, bidi_mode=True) - # Der neue Strom darf nicht höher als der in dieser Stufe bisher gesetzter sein - current = self.get_current_to_set( + # Der neue Strom darf nicht höher als der bisher gesetzte Strom sein + current = common.get_current_to_set( cp.data.set.current, available_for_cp, cp.data.set.target_current) # Ausgabe LIMIT-MSG @@ -58,16 +59,11 @@ def set_bidi(self): cp.data.set.current = current log.info(f"LP{cp.num}: Stromstärke {current}A") - log.debug(f"cp {cp.num} available currents {available_currents} missing currents " - f"{missing_currents} limit {limit.message} -----counter_{counter}" - f"CURRENT {current}A") - - log.debug(f"_After_check________LP{cp.num}: available currents {current}A") - - x = [current, current, current] + required_currents = cp.data.control_parameter.required_currents + cp_phase_currents = [current if required_currents[i] != 0 else 0 for i in range(3)] grid_counter.update_surplus_values_left( - x, voltages_mean(cp.data.get.voltages)) + cp_phase_currents, voltages_mean(cp.data.get.voltages)) preferenced_cps.pop(0) @@ -84,10 +80,10 @@ def _set_loadmangement_message(self, round(current, 2) != round(max( chargepoint.data.control_parameter.required_currents), 2)): if current < 0: - chargepoint.set_state_and_log(f"MY_Es kann nicht mit der vorgegebenen Stromstärke entladen werden" + chargepoint.set_state_and_log(f"Es kann nicht mit der vorgegebenen Stromstärke entladen werden" f"{limit.message}") else: - chargepoint.set_state_and_log(f"MY_Es kann nicht mit der vorgegebenen Stromstärke geladen werden" + chargepoint.set_state_and_log(f"Es kann nicht mit der vorgegebenen Stromstärke geladen werden" f"{limit.message}") def get_counts(self, chargepoint: Chargepoint) -> List[int]: @@ -103,7 +99,7 @@ def get_missing_currents(self, preferenced_cps: List[Chargepoint], grid_counter) cp = preferenced_cps[0] missing_currents = [0, 0, 0] if cp.data.control_parameter.chargemode == Chargemode.INSTANT_CHARGING: - log.debug(f"ÜBERSCHUSS_ALL für LP{cp.num}: {grid_counter.data.set.surplus_power_left }W") + # Entladen im Instant-Charging-Modus if cp.data.set.charging_ev_data.data.get.soc > 0: # Auto-bat ist nicht leer @@ -120,7 +116,6 @@ def get_missing_currents(self, preferenced_cps: List[Chargepoint], grid_counter) else: # Default Nullpunktanpassung zero_point_adjustment = grid_counter.data.set.surplus_power_left / len(preferenced_cps) - log.debug(f"ÜBERSCHUSS_ALL für LP{cp.num}: {grid_counter.data.set.surplus_power_left }W") log.debug(f"Nullpunktanpassung für LP{cp.num}: verbleibende Leistung {zero_point_adjustment}W") missing_currents = [zero_point_adjustment / cp.data.get.phases_in_use / 230 for i in range(0, cp.data.get.phases_in_use)] @@ -140,44 +135,3 @@ def get_missing_currents(self, preferenced_cps: List[Chargepoint], grid_counter) cp.data.get.phases_in_use) return missing_currents - - def get_current_to_set(self, set_current: float, diff: float, prev_current: float) -> float: - """Der neue Strom darf nicht höher als der in dieser Stufe bisher gesetzter sein, - um das LM der untergeordneten Zähler nicht zu untergraben. Der Vergleich muss positiv - sein, wenn zum ersten Mal auf dieser Stufe ein Strom gesetzt wird.""" - new_current = prev_current + diff - if set_current is not None: - if diff < 0: - if new_current < set_current: - log.debug("Neuer Soll-Strom darf beim Entladen nicht niedriger als bisher gesetzter sein: " - f"bisher {set_current}A, neuer {new_current}") - return set_current - else: - if new_current > set_current: - log.debug("BIDI_Neuer Soll-Strom darf nicht höher als bisher gesetzter sein: " - f"bisher {set_current}A, neuer {new_current}") - return set_current - return new_current - - def available_current_for_bidi_cp(self, chargepoint: Chargepoint, - counts: List[int], - available_currents: List[float], - missing_currents: List[float]) -> float: - - # control_parameter.required_current - # -> ist immer positve aktuell und gibt die maximal benötigte Stromstärke an - control_parameter = chargepoint.data.control_parameter - missing_current_cp = control_parameter.required_current - chargepoint.data.set.target_current - is_discharge = missing_current_cp < 0 - available_current = float("-inf") if is_discharge else float("inf") - for i in range(0, 3): - phase_available_current = available_currents[i] / counts[i] - if is_discharge: - available_current = max( - max(missing_current_cp, phase_available_current), available_current) - else: - available_current = min( - min(missing_current_cp, phase_available_current), available_current) - if available_current in [float("inf"), float("-inf")]: - available_current = missing_current_cp - return available_current diff --git a/packages/control/algorithm/chargemodes.py b/packages/control/algorithm/chargemodes.py index 8c033a5709..9966b55c16 100644 --- a/packages/control/algorithm/chargemodes.py +++ b/packages/control/algorithm/chargemodes.py @@ -19,17 +19,17 @@ (Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, True), # 14 (Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, False), # 15 # niedrigere Priorität soll nachrangig geladen, aber zuerst entladen werden - (Chargemode.INSTANT_CHARGING, Chargemode.BIDI_CHARGING, False), # 18 - (Chargemode.INSTANT_CHARGING, Chargemode.BIDI_CHARGING, True), # 19 - (Chargemode.SCHEDULED_CHARGING, Chargemode.BIDI_CHARGING, False), # 16 - (Chargemode.SCHEDULED_CHARGING, Chargemode.BIDI_CHARGING, True), # 17 + (Chargemode.INSTANT_CHARGING, Chargemode.BIDI_CHARGING, False), # 16 + (Chargemode.INSTANT_CHARGING, Chargemode.BIDI_CHARGING, True), # 17 + (Chargemode.SCHEDULED_CHARGING, Chargemode.BIDI_CHARGING, False), # 18 + (Chargemode.SCHEDULED_CHARGING, Chargemode.BIDI_CHARGING, True), # 19 (None, Chargemode.STOP, True), # 20 (None, Chargemode.STOP, False)) # 21 CONSIDERED_CHARGE_MODES_SURPLUS = CHARGEMODES[0:2] + CHARGEMODES[6:16] CONSIDERED_CHARGE_MODES_PV_ONLY = CHARGEMODES[10:16] -CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT = CHARGEMODES[0:10] # + CHARGEMODES[16:20] -CONSIDERED_CHARGE_MODES_MIN_CURRENT = CHARGEMODES[0:-6] # + CHARGEMODES[16:18] +CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT = CHARGEMODES[0:10] +CONSIDERED_CHARGE_MODES_MIN_CURRENT = CHARGEMODES[0:-6] CONSIDERED_CHARGE_MODES_NO_CURRENT = CHARGEMODES[20:22] CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE = CHARGEMODES[16:20] CONSIDERED_CHARGE_MODES_CHARGING = CHARGEMODES[0:16] diff --git a/packages/control/algorithm/common.py b/packages/control/algorithm/common.py index 60f81c7fe5..4a41dd823e 100644 --- a/packages/control/algorithm/common.py +++ b/packages/control/algorithm/common.py @@ -95,10 +95,16 @@ def get_current_to_set(set_current: float, diff: float, prev_current: float) -> wird.""" new_current = prev_current + diff if set_current is not None: - if new_current > set_current: - log.debug("Neuer Soll-Strom darf nicht höher als bisher gesetzter sein: " - f"bisher {set_current}A, neuer {new_current}") - return set_current + if diff < 0: + if new_current < set_current: + log.debug("Neuer Soll-Strom darf beim Entladen nicht niedriger als bisher gesetzter sein: " + f"bisher {set_current}A, neuer {new_current}") + return set_current + else: + if new_current > set_current: + log.debug("Neuer Soll-Strom darf nicht höher als bisher gesetzter sein: " + f"bisher {set_current}A, neuer {new_current}") + return set_current return new_current # tested @@ -107,10 +113,29 @@ def get_current_to_set(set_current: float, diff: float, prev_current: float) -> def available_current_for_cp(chargepoint: Chargepoint, counts: List[int], available_currents: List[float], - missing_currents: List[float]) -> float: + missing_currents: List[float], + bidi_mode: bool = False) -> float: control_parameter = chargepoint.data.control_parameter - available_current = float("inf") missing_current_cp = control_parameter.required_current - chargepoint.data.set.target_current + + if bidi_mode: + is_discharge = missing_current_cp < 0 + available_current = float("-inf") if is_discharge else float("inf") + for i in range(0, 3): + if control_parameter.required_currents[i] == 0 or counts[i] == 0: + continue + phase_available_current = available_currents[i] / counts[i] + if is_discharge: + available_current = max( + max(missing_current_cp, phase_available_current), available_current) + else: + available_current = min( + min(missing_current_cp, phase_available_current), available_current) + if available_current in [float("inf"), float("-inf")]: + available_current = missing_current_cp + return available_current + + available_current = float("inf") for i in range(0, 3): if (control_parameter.required_currents[i] != 0 and missing_currents[i] != available_currents[i]): diff --git a/packages/control/algorithm/integration_test/bidi_charging_test.py b/packages/control/algorithm/integration_test/bidi_charging_test.py index 0a065ac734..1f2a38e341 100644 --- a/packages/control/algorithm/integration_test/bidi_charging_test.py +++ b/packages/control/algorithm/integration_test/bidi_charging_test.py @@ -2,6 +2,7 @@ from unittest.mock import Mock from control import data +from control import loadmanagement from control.algorithm.algorithm import Algorithm from control.chargemode import Chargemode @@ -9,6 +10,11 @@ @pytest.fixture() def bidi_cps(): def _setup(*cps): + for counter in ("counter0", "counter6"): + data.data.counter_data[counter].data.set.raw_currents_left = [32]*3 + data.data.counter_data[counter].data.set.raw_exported_currents_left = [32]*3 + data.data.counter_data[counter].data.set.raw_power_left = 22000 + data.data.counter_data[counter].data.set.raw_exported_power_left = 22000 for cp in cps: data.data.cp_data[cp].data.get.max_discharge_power = -11000 data.data.cp_data[cp].data.get.max_charge_power = 11000 @@ -30,7 +36,9 @@ def test_cp3_bidi(grid_power: float, expected_current: float, bidi_cps, all_cp_n # setup bidi_cps("cp3") data.data.counter_data["counter0"].data.get.power = grid_power - return_mock = Mock(reurn_value=True) + return_mock = Mock(return_value=True) + mock_get_component_name_by_id = Mock(return_value="Garage") + monkeypatch.setattr(loadmanagement, "get_component_name_by_id", mock_get_component_name_by_id) monkeypatch.setattr( data.data.cp_data["cp3"].data.set.charging_ev_data.charge_template, "bidi_charging_allowed", return_mock) @@ -48,6 +56,8 @@ def test_cp3_cp4_bidi_discharge(bidi_cps, all_cp_not_charging, monkeypatch): # setup bidi_cps("cp3", "cp4") data.data.counter_data["counter0"].data.get.power = 4000 + mock_get_component_name_by_id = Mock(return_value="Garage") + monkeypatch.setattr(loadmanagement, "get_component_name_by_id", mock_get_component_name_by_id) # execution Algorithm().calc_current() diff --git a/packages/control/counter.py b/packages/control/counter.py index 4a089f7c9f..8e30b0e4a9 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -149,8 +149,6 @@ def _get_loadmanagement_state(self) -> None: def _set_current_left(self, loadmanagement_available: bool) -> None: if loadmanagement_available: - if self.num == 26: - log.debug("-.--.-.-.-") currents_raw = self.data.get.currents currents_exported_raw = self.data.get.currents cp_keys = data.data.counter_all_data.get_chargepoints_of_counter(f"counter{self.num}") @@ -163,34 +161,20 @@ def _set_current_left(self, loadmanagement_available: bool) -> None: except KeyError: element_current = [get_medium_charging_current(chargepoint.data.get.currents)]*3 - # Ich schau, was liegt aktuell am Zähler an -> currents_raw - # Dann ziehe ich davon alle cps ab -> currents_raw = list(map(operator.sub, currents_raw, element_current)) - # => dann habe ich in currents_raw nur noch die Ströme die nicht zum laden verwendet werden if min(element_current) < 0: - # currents_exported_raw = alles ohne entladen -> sprich nur die Hausverbraucher ohne Einspeisung + # nur Hausverbraucher ohne Einspeisung currents_exported_raw = list(map(operator.sub, currents_exported_raw, element_current)) continue - # currents_raw = alles ohne laden -> sprich nur die Hausverbraucher und Einspeisung + # nur Hausverbraucher und Einspeisung currents_raw = list(map(operator.sub, currents_raw, element_current)) - # Diese ziehe ich dann von den max_wert des Counters ab - # -> Bekomme die verbleibenden Ströme die zum Laden verwendet werden können - - # Das Entladen steckt ja schon in currents_raw drin -> direkter Zählerwert - # Dadurch das nur die wirklich ladenen Cps rausrechnen werden -> bleibt Hausverbrauch und Entladen übrig - # Dann max-Wert vom Counter minus Hausverbrauch und Entladen -> Verbleibende Ströme die zum Laden verwendet werden können - # Wenn mehr entladen als Hausverbrauch -> dann ist der Wert negativ - # Und das limit am Counter erhöht sich -> mehr Strom zum Laden verfügbar - raw_currents_left = list(map(operator.sub, self.data.config.max_currents, currents_raw)) - - # Addiere alle Verbraucher dazu, weil wir das ja mehr entladen können dann raw_exported_currents_left = list(map(operator.add, self.data.config.max_currents, currents_exported_raw)) if min(raw_currents_left) < 0: log.debug( - f"Verbleibende Ströme Laden: {raw_currents_left}, " - f"Überbelastung wird durch Hausverbrauch verursacht") + f"Verbleibende Ströme Laden: " + f"{raw_currents_left}, Überbelastung wird durch Hausverbrauch verursacht") raw_currents_left = [max(raw_currents_left[i], 0) for i in range(0, 3)] if min(raw_exported_currents_left) < 0: @@ -228,12 +212,14 @@ def _set_power_left(self, loadmanagement_available: bool) -> None: if f'counter{self.num}' == data.data.counter_all_data.get_evu_counter_str(): if loadmanagement_available: power_raw = self.data.get.power + power_exported_raw = self.data.get.power for cp in data.data.cp_data.values(): if cp.data.get.power < 0: + power_exported_raw -= cp.data.get.power continue power_raw -= cp.data.get.power self.data.set.raw_power_left = self.data.config.max_total_power - power_raw - self.data.set.raw_exported_power_left = self.data.config.max_total_power + power_raw + self.data.set.raw_exported_power_left = self.data.config.max_total_power + power_exported_raw log.info(f'Verbleibende Leistung an Zähler {self.num}: {self.data.set.raw_power_left}W') log.info((f'Verbleibende exportierte Leistung an Zähler {self.num}: ' f'{self.data.set.raw_exported_power_left}W')) @@ -248,34 +234,32 @@ def _set_power_left(self, loadmanagement_available: bool) -> None: self.data.set.raw_exported_power_left = None def update_values_left(self, diffs, cp_voltage: float) -> None: - # Mittelwert der Spannungen verwenden, um Phasenverdrehung zu kompensieren - # (Probleme bei einphasig angeschlossenen Wallboxen) - self._update_raw_values(diffs, cp_voltage) + self._update_raw_values(diffs, cp_voltage, surplus=False) def update_surplus_values_left(self, diffs, cp_voltage: float) -> None: - self._update_raw_values(diffs, cp_voltage) - - if self.data.set.surplus_power_left is not None: - self.data.set.surplus_power_left -= sum([c * cp_voltage for c in diffs]) - - log.debug(f'Zähler {self.num}: {self.data.set.raw_currents_left}A verbleibende Ströme, ' - f'{self.data.set.surplus_power_left}W verbleibender Überschuss') - - def _update_raw_values(self, diffs, cp_voltage: float) -> None: + self._update_raw_values(diffs, cp_voltage, surplus=True) + def _update_raw_values(self, diffs, cp_voltage: float, surplus: bool = False) -> None: + # Mittelwert der Spannungen verwenden, um Phasenverdrehung zu kompensieren + # (Probleme bei einphasig angeschlossenen Wallboxen) self.data.set.raw_currents_left = list(map(operator.sub, self.data.set.raw_currents_left, diffs)) self.data.set.raw_exported_currents_left = list( map(operator.add, self.data.set.raw_exported_currents_left, diffs)) - if self.data.set.raw_power_left is not None: - self.data.set.raw_power_left -= sum([c * cp_voltage for c in diffs]) - if self.data.set.raw_exported_power_left is not None: - self.data.set.raw_exported_power_left += sum([c * cp_voltage for c in diffs]) - - log.debug(f'Zähler {self.num}: {self.data.set.raw_currents_left}A verbleibende Ströme, ' - f'{self.data.set.raw_exported_currents_left}A verbleibende exportierte Ströme, ' - f'{self.data.set.raw_power_left}W verbleibende Leistung, ' - f'{self.data.set.raw_exported_power_left}W verbleibende exportierte Leistung') + if surplus: + if self.data.set.surplus_power_left is not None: + self.data.set.surplus_power_left -= sum([c * cp_voltage for c in diffs]) + log.debug(f'Zähler {self.num}: {self.data.set.raw_currents_left}A verbleibende Ströme, ' + f'{self.data.set.surplus_power_left}W verbleibender Überschuss') + else: + if self.data.set.raw_power_left is not None: + self.data.set.raw_power_left -= sum([c * cp_voltage for c in diffs]) + if self.data.set.raw_exported_power_left is not None: + self.data.set.raw_exported_power_left += sum([c * cp_voltage for c in diffs]) + log.debug(f'Zähler {self.num}: {self.data.set.raw_currents_left}A verbleibende Ströme, ' + f'{self.data.set.raw_exported_currents_left}A verbleibende exportierte Ströme, ' + f'{self.data.set.raw_power_left}W verbleibende Leistung, ' + f'{self.data.set.raw_exported_power_left}W verbleibende exportierte Leistung') def calc_surplus(self): # reservierte Leistung wird nicht berücksichtigt, weil diese noch verwendet werden kann, bis die EV diff --git a/packages/control/ev/charge_template_test.py b/packages/control/ev/charge_template_test.py index 5fc859e02b..b6989b5467 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -132,7 +132,7 @@ def test_instant_charging(selected: str, current_soc: float, used_amount: float, ct.data.chargemode.instant_charging.limit.amount = 1000 # execution - ret = ct.instant_charging(current_soc, used_amount, ChargingType.AC.value) + ret = ct.instant_charging(current_soc, used_amount, ChargingType.AC.value, BidiState.CP_NOT_BIDI_CAPABLE) # evaluation assert ret == expected diff --git a/packages/control/loadmanagement.py b/packages/control/loadmanagement.py index 4f5dd93a28..a008b829e0 100644 --- a/packages/control/loadmanagement.py +++ b/packages/control/loadmanagement.py @@ -76,78 +76,20 @@ def get_available_currents_bidi(self, missing_currents: List[float], cp_voltage: float, counter: Counter, - cp: Chargepoint, feed_in: int = 0) -> Tuple[List[float], LoadmanagementLimit]: raw_currents_left = counter.data.set.raw_currents_left - raw_exported_currents_left = counter.data.set.raw_exported_currents_left - - available_currents, limit = self._limit_by_current_bidi( - counter, missing_currents, raw_currents_left, raw_exported_currents_left) + available_currents, limit = self._limit_by_current(counter, missing_currents, raw_currents_left) - available_currents, new_limit = self._limit_by_power_bidi( - counter, - available_currents, - cp_voltage, - counter.data.set.raw_power_left, - counter.data.set.raw_exported_power_left, - feed_in) + available_currents, new_limit = self._limit_by_power( + counter, available_currents, cp_voltage, counter.data.set.raw_power_left, feed_in) limit = new_limit if new_limit.limiting_value is not None else limit - return available_currents, limit - - def _limit_by_power_bidi(self, - counter: Counter, - available_currents: List[float], - cp_voltage: float, - raw_power_left: Optional[float], - raw_exported_power_left: Optional[float], - feed_in: Optional[float]) -> Tuple[List[float], LoadmanagementLimit]: - # Mittelwert der Spannungen verwenden, um Phasenverdrehung zu kompensieren - # (Probleme bei einphasig angeschlossenen Wallboxen) - currents = available_currents.copy() - limit = LoadmanagementLimit(None, None) - total_power = sum([c * cp_voltage for c in available_currents]) - power_left = raw_power_left if total_power >= 0 else raw_exported_power_left - - if power_left is None: - return currents, limit - elif power_left > 0: - if feed_in is not None: - power_left = max(power_left - feed_in, 0) - log.debug(f"Verbleibende Leistung unter Berücksichtigung der Einspeisegrenze: {power_left}W") - if abs(total_power) > power_left: - for i in range(0, 3): - try: - # Am meisten belastete Phase trägt am meisten zur Leistungsreduktion bei. - currents[i] = available_currents[i] / sum(available_currents) * power_left / cp_voltage - except ZeroDivisionError: - # bei einphasig angeschlossenen Wallboxen ist die Spannung der anderen Phasen 0V - currents[i] = 0.0 - log.debug(f"Leistungsüberschreitung auf {power_left}W korrigieren: {available_currents}") - limit = LoadmanagementLimit(LimitingValue.POWER.value.format(get_component_name_by_id(counter.num)), - LimitingValue.POWER) - return currents, limit - else: - return [0]*3, LoadmanagementLimit(LimitingValue.POWER.value.format(get_component_name_by_id(counter.num)), - LimitingValue.POWER) + if f"counter{counter.num}" == data.data.counter_all_data.get_evu_counter_str(): + available_currents, new_limit = self._limit_by_unbalanced_load( + counter, available_currents, raw_currents_left, + len([value for value in missing_currents if value != 0])) + limit = new_limit if new_limit.limiting_value is not None else limit - # tested - def _limit_by_current_bidi(self, - counter: Counter, - missing_currents: List[float], - raw_currents_left: List[float], - raw_exported_currents_left: List[float]) -> Tuple[List[float], LoadmanagementLimit]: - available_currents = [0.0]*3 - limit = LoadmanagementLimit(None, None) - for i in range(0, 3): - if missing_currents[i] >= 0: - available_currents[i] = min(missing_currents[i], raw_currents_left[i]) - else: - available_currents[i] = max(missing_currents[i], -raw_exported_currents_left[i]) - if available_currents != missing_currents: - log.debug(f"Stromüberschreitung {missing_currents}W korrigieren: {available_currents}") - limit = LoadmanagementLimit(LimitingValue.CURRENT.value.format(get_component_name_by_id(counter.num)), - LimitingValue.CURRENT) return available_currents, limit def _limit_by_unbalanced_load(self, @@ -180,21 +122,25 @@ def _limit_by_power(self, # (Probleme bei einphasig angeschlossenen Wallboxen) currents = available_currents.copy() limit = LoadmanagementLimit(None, None) - if raw_power_left is None: + raw_exported_power_left = counter.data.set.raw_exported_power_left + total_power = sum([c * cp_voltage for c in available_currents]) + power_left = raw_power_left if total_power >= 0 else raw_exported_power_left + + if power_left is None: return currents, limit - elif raw_power_left > 0: + elif power_left > 0: if feed_in is not None: - raw_power_left = max(raw_power_left - feed_in, 0) - log.debug(f"Verbleibende Leistung unter Berücksichtigung der Einspeisegrenze: {raw_power_left}W") - if sum([c * cp_voltage for c in available_currents]) > raw_power_left: + power_left = max(power_left - feed_in, 0) + log.debug(f"Verbleibende Leistung unter Berücksichtigung der Einspeisegrenze: {power_left}W") + if abs(total_power) > power_left: for i in range(0, 3): try: # Am meisten belastete Phase trägt am meisten zur Leistungsreduktion bei. - currents[i] = available_currents[i] / sum(available_currents) * raw_power_left / cp_voltage + currents[i] = available_currents[i] / sum(available_currents) * power_left / cp_voltage except ZeroDivisionError: # bei einphasig angeschlossenen Wallboxen ist die Spannung der anderen Phasen 0V currents[i] = 0.0 - log.debug(f"Leistungsüberschreitung auf {raw_power_left}W korrigieren: {available_currents}") + log.debug(f"Leistungsüberschreitung auf {power_left}W korrigieren: {available_currents}") limit = LoadmanagementLimit(LimitingValue.POWER.value.format(get_component_name_by_id(counter.num)), LimitingValue.POWER) return currents, limit @@ -208,9 +154,13 @@ def _limit_by_current(self, missing_currents: List[float], raw_currents_left: List[float]) -> Tuple[List[float], LoadmanagementLimit]: available_currents = [0.0]*3 + raw_exported_currents_left = counter.data.set.raw_exported_currents_left limit = LoadmanagementLimit(None, None) for i in range(0, 3): - available_currents[i] = min(missing_currents[i], raw_currents_left[i]) + if missing_currents[i] >= 0: + available_currents[i] = min(missing_currents[i], raw_currents_left[i]) + else: + available_currents[i] = max(missing_currents[i], -raw_exported_currents_left[i]) if available_currents != missing_currents: log.debug(f"Stromüberschreitung {missing_currents}W korrigieren: {available_currents}") limit = LoadmanagementLimit(LimitingValue.CURRENT.value.format(get_component_name_by_id(counter.num)), diff --git a/packages/modules/common/store/_chargepoint.py b/packages/modules/common/store/_chargepoint.py index 728c44fdda..4705dfbb0b 100644 --- a/packages/modules/common/store/_chargepoint.py +++ b/packages/modules/common/store/_chargepoint.py @@ -44,10 +44,12 @@ def update(self): pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/max_evse_current", self.state.max_evse_current) # ONLY_TEST Wieder rückgängig machen pub_to_broker("openWB/set/chargepoint/" + str(self.num) + - "/get/max_charge_power", 500000 if self.state.max_charge_power is None else self.state.max_charge_power) + "/get/max_charge_power", + 500000 if self.state.max_charge_power is None else self.state.max_charge_power) # ONLY_TEST Wieder rückgängig machen pub_to_broker("openWB/set/chargepoint/" + str(self.num) + - "/get/max_discharge_power", -500000 if self.state.max_discharge_power is None else self.state.max_discharge_power) + "/get/max_discharge_power", + -500000 if self.state.max_discharge_power is None else self.state.max_discharge_power) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/version", self.state.version) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/current_branch", self.state.current_branch) From c77c482876f0a45a4c8b298cfe2d7b56bb2201fd Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Wed, 12 Aug 2026 09:26:36 +0200 Subject: [PATCH 6/8] Remove local debugging setup --- packages/modules/common/store/_chargepoint.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/modules/common/store/_chargepoint.py b/packages/modules/common/store/_chargepoint.py index 4705dfbb0b..292887f31b 100644 --- a/packages/modules/common/store/_chargepoint.py +++ b/packages/modules/common/store/_chargepoint.py @@ -42,22 +42,13 @@ def update(self): pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/evse_current", self.state.evse_current) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/vehicle_id", self.state.vehicle_id) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/max_evse_current", self.state.max_evse_current) - # ONLY_TEST Wieder rückgängig machen - pub_to_broker("openWB/set/chargepoint/" + str(self.num) + - "/get/max_charge_power", - 500000 if self.state.max_charge_power is None else self.state.max_charge_power) - # ONLY_TEST Wieder rückgängig machen - pub_to_broker("openWB/set/chargepoint/" + str(self.num) + - "/get/max_discharge_power", - -500000 if self.state.max_discharge_power is None else self.state.max_discharge_power) - + pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/max_charge_power", self.state.max_charge_power) + pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/max_discharge_power", + self.state.max_discharge_power) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/version", self.state.version) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/current_branch", self.state.current_branch) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/current_commit", self.state.current_commit) - - # ONLY_TEST Wieder rückgängig machen - pub_to_broker("openWB/set/chargepoint/" + str(self.num) + - "/get/evse_signaling", "HLC") # self.state.evse_signaling) # + pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/evse_signaling", self.state.evse_signaling) def get_chargepoint_value_store(id: int) -> ValueStore[ChargepointState]: From 6e52bad4984fc4d4f4b46230f01fa95ce6959c32 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Wed, 12 Aug 2026 12:30:29 +0200 Subject: [PATCH 7/8] Add tests and fix bidi_charging --- packages/control/algorithm/bidi_charging.py | 22 +-- packages/control/algorithm/common.py | 5 +- .../integration_test/bidi_charging_test.py | 136 ++++++++++++++++++ 3 files changed, 149 insertions(+), 14 deletions(-) diff --git a/packages/control/algorithm/bidi_charging.py b/packages/control/algorithm/bidi_charging.py index 277d810640..e5f6e8ebb6 100644 --- a/packages/control/algorithm/bidi_charging.py +++ b/packages/control/algorithm/bidi_charging.py @@ -53,17 +53,13 @@ def set_bidi(self): current = common.get_current_to_set( cp.data.set.current, available_for_cp, cp.data.set.target_current) - # Ausgabe LIMIT-MSG - self._set_loadmangement_message(current, limit, cp) - cp.data.set.current = current log.info(f"LP{cp.num}: Stromstärke {current}A") - required_currents = cp.data.control_parameter.required_currents - cp_phase_currents = [current if required_currents[i] != 0 else 0 for i in range(3)] + # Ausgabe LIMIT-MSG + self._set_loadmangement_message(current, limit, cp) - grid_counter.update_surplus_values_left( - cp_phase_currents, voltages_mean(cp.data.get.voltages)) + common.set_current_counterdiff(cp.data.set.target_current, current, cp, surplus=True) preferenced_cps.pop(0) @@ -75,11 +71,15 @@ def _set_loadmangement_message(self, log.debug( f"current {current} target {chargepoint.data.set.target_current} set current {chargepoint.data.set.current}" f" required currents {chargepoint.data.control_parameter.required_currents}") - if (limit.message and current != max(chargepoint.data.set.target_current, chargepoint.data.set.current or 0) and + required_currents = chargepoint.data.control_parameter.required_currents + required_current = min(required_currents) if current < 0 else max(required_currents) + if (limit.message and # Strom erreicht nicht die vorgegebene Stromstärke - round(current, 2) != round(max( - chargepoint.data.control_parameter.required_currents), 2)): - if current < 0: + round(current, 2) != round(required_current, 2)): + if current == 0: + chargepoint.set_state_and_log(f"Es kann nicht mit der vorgegebenen Stromstärke geladen/entladen werden" + f"{limit.message}") + elif current < 0: chargepoint.set_state_and_log(f"Es kann nicht mit der vorgegebenen Stromstärke entladen werden" f"{limit.message}") else: diff --git a/packages/control/algorithm/common.py b/packages/control/algorithm/common.py index 4a41dd823e..8380eee9a8 100644 --- a/packages/control/algorithm/common.py +++ b/packages/control/algorithm/common.py @@ -67,10 +67,9 @@ def set_current_counterdiff(diff_current: float, required_currents = chargepoint.data.control_parameter.required_currents considered_current = consider_less_charging_chargepoint_in_loadmanagement( chargepoint, current) - # gar nicht ladende Autos? - diff = max(considered_current - diff_current, 0) + diff = considered_current - diff_current diffs = [diff if required_currents[i] != 0 else 0 for i in range(3)] - if max(diffs) > 0: + if any(diff_value != 0 for diff_value in diffs): counters = data.data.counter_all_data.get_counters_to_check(chargepoint.num) for counter in counters: if surplus: diff --git a/packages/control/algorithm/integration_test/bidi_charging_test.py b/packages/control/algorithm/integration_test/bidi_charging_test.py index 1f2a38e341..1c1906b0af 100644 --- a/packages/control/algorithm/integration_test/bidi_charging_test.py +++ b/packages/control/algorithm/integration_test/bidi_charging_test.py @@ -67,3 +67,139 @@ def test_cp3_cp4_bidi_discharge(bidi_cps, all_cp_not_charging, monkeypatch): assert data.data.cp_data["cp4"].data.set.current == -2.898550724637681 assert data.data.cp_data["cp5"].data.set.current == 0 assert data.data.counter_data["counter0"].data.set.surplus_power_left == 0 + + +def test_cp3_bidi_instant_discharge_uses_dc_current(bidi_cps, all_cp_not_charging, monkeypatch): + # Testet, ob das Entladen entsprechend vom max_discharge_power limitiert wird + + # setup + bidi_cps("cp3") + control_parameter = data.data.cp_data["cp3"].data.control_parameter + control_parameter.chargemode = Chargemode.INSTANT_CHARGING + data.data.cp_data["cp3"].data.set.charging_ev_data.data.get.soc = 80 + data.data.cp_data["cp3"].data.set.charging_ev_data.charge_template.data.chargemode.instant_charging.dc_current = -20 + mock_get_component_name_by_id = Mock(return_value="Garage") + monkeypatch.setattr(loadmanagement, "get_component_name_by_id", mock_get_component_name_by_id) + + # execution + Algorithm().calc_current() + + # evaluation + assert data.data.cp_data["cp3"].data.set.current == -15.942028985507246 + assert data.data.cp_data["cp4"].data.set.current == 0 + assert data.data.cp_data["cp5"].data.set.current == 0 + assert data.data.counter_data["counter0"].data.set.surplus_power_left == 10310.0 + + +def test_cp3_bidi_instant_discharge_limited_by_counter_export_current( + bidi_cps, all_cp_not_charging, monkeypatch): + # Testet, ob das Entladen entsprechend vom Counter limitiert wird + + # setup + bidi_cps("cp3") + control_parameter = data.data.cp_data["cp3"].data.control_parameter + control_parameter.chargemode = Chargemode.INSTANT_CHARGING + data.data.cp_data["cp3"].data.get.max_discharge_power = -22000 + data.data.cp_data["cp3"].data.set.charging_ev_data.data.get.soc = 80 + data.data.cp_data["cp3"].data.set.charging_ev_data.charge_template.data.chargemode.instant_charging.dc_current = -20 + for counter in ("counter0", "counter6"): + data.data.counter_data[counter].data.set.raw_exported_currents_left = [10]*3 + mock_get_component_name_by_id = Mock(return_value="Garage") + monkeypatch.setattr(loadmanagement, "get_component_name_by_id", mock_get_component_name_by_id) + + # execution + Algorithm().calc_current() + + # evaluation + assert data.data.cp_data["cp3"].data.set.current == -10 + assert data.data.cp_data["cp4"].data.set.current == 0 + assert data.data.cp_data["cp5"].data.set.current == 0 + + +@pytest.mark.parametrize( + "max_discharge_power, expected_cp4_current, expected_cp5_current", + [ + # Begrenzt durch max_discharge_power und Counter + pytest.param(-11000, -15.942028985507246, -4.057971014492754, id="max_discharge_11kW"), + # Nur durch Counter begrenzt + pytest.param(-110000, -20, 0, id="max_discharge_110kW"), + ], +) +def test_cp3_cp4_bidi_instant_discharge_splits_limited_export_current( + max_discharge_power, expected_cp4_current, expected_cp5_current, + bidi_cps, all_cp_not_charging, monkeypatch): + # Beide CPs sind auf einer Ebene und wollen -20 A entladen + # Counter erlaubt insgesamt nur -20 A pro Phase + + # setup + bidi_cps("cp4", "cp5") + for cp in ("cp4", "cp5"): + control_parameter = data.data.cp_data[cp].data.control_parameter + control_parameter.chargemode = Chargemode.INSTANT_CHARGING + data.data.cp_data[cp].data.get.max_discharge_power = max_discharge_power + data.data.cp_data[cp].data.set.charging_ev_data.data.get.soc = 80 + (data.data.cp_data[cp].data.set.charging_ev_data.charge_template.data + .chargemode.instant_charging.dc_current) = -20 + + for counter in ("counter0", "counter6"): + data.data.counter_data[counter].data.set.raw_exported_currents_left = [20]*3 + mock_get_component_name_by_id = Mock(return_value="Garage") + monkeypatch.setattr(loadmanagement, "get_component_name_by_id", mock_get_component_name_by_id) + + # execution + Algorithm().calc_current() + + # evaluation + assert data.data.cp_data["cp3"].data.set.current == 0 + assert data.data.cp_data["cp4"].data.set.current == expected_cp4_current + assert data.data.cp_data["cp5"].data.set.current == expected_cp5_current + + +def test_cp4_bidi_discharge_unlocks_cp5_instant_charging_in_next_cycle( + bidi_cps, all_cp_not_charging, monkeypatch): + # Zyklus 1: CP4 entlaedt, CP5 bleibt bei 20 A (Counter-Limit 20 A). + # Zyklus 2: Entladung ist am Counter reflektiert, dadurch kann CP5 auf 40 A steigen. + + # setup + bidi_cps("cp4") + cp4_control_parameter = data.data.cp_data["cp4"].data.control_parameter + cp4_control_parameter.chargemode = Chargemode.INSTANT_CHARGING + data.data.cp_data["cp4"].data.get.max_discharge_power = -22000 + data.data.cp_data["cp4"].data.set.charging_ev_data.data.get.soc = 80 + data.data.cp_data["cp4"].data.set.charging_ev_data.charge_template.data.chargemode.instant_charging.dc_current = -20 + + cp5 = data.data.cp_data["cp5"] + cp5_control_parameter = cp5.data.control_parameter + cp5_control_parameter.chargemode = Chargemode.INSTANT_CHARGING + cp5_control_parameter.submode = Chargemode.INSTANT_CHARGING + cp5_control_parameter.phases = 3 + cp5_control_parameter.required_currents = [50]*3 + cp5_control_parameter.required_current = 50 + cp5.data.get.max_charge_power = 110000 + cp5.data.get.charge_state = True + cp5.data.get.currents = [20]*3 + cp5.data.set.charging_ev_data.ev_template.data.max_current_multi_phases = 50 + cp5.template.data.max_current_multi_phases = 50 + + for counter in ("counter0", "counter6"): + data.data.counter_data[counter].data.set.raw_currents_left = [20]*3 + data.data.counter_data[counter].data.set.raw_exported_currents_left = [20]*3 + data.data.counter_data[counter].data.set.raw_power_left = 100000 + data.data.counter_data[counter].data.set.raw_exported_power_left = 100000 + + mock_get_component_name_by_id = Mock(return_value="Garage") + monkeypatch.setattr(loadmanagement, "get_component_name_by_id", mock_get_component_name_by_id) + + # execution + evaluation cycle 1 + Algorithm().calc_current() + assert data.data.cp_data["cp4"].data.set.current == -20 + assert data.data.cp_data["cp5"].data.set.current == 20 + + # Entladung aus Zyklus 1 ist im naechsten Zyklus als zusaetzlicher Spielraum verfuegbar. + for counter in ("counter0", "counter6"): + data.data.counter_data[counter].data.set.raw_currents_left = [40]*3 + + # execution + evaluation cycle 2 + Algorithm().calc_current() + assert data.data.cp_data["cp4"].data.set.current == -20 + assert data.data.cp_data["cp5"].data.set.current == 40 From 5fe579b1b2ddeb8835980ef50a4994d615de5bf8 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Mon, 17 Aug 2026 08:22:31 +0200 Subject: [PATCH 8/8] Add Copilot suggestions --- packages/control/algorithm/bidi_charging.py | 2 ++ packages/control/loadmanagement.py | 5 ++++- packages/helpermodules/setdata.py | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/control/algorithm/bidi_charging.py b/packages/control/algorithm/bidi_charging.py index e5f6e8ebb6..c1695b5ec3 100644 --- a/packages/control/algorithm/bidi_charging.py +++ b/packages/control/algorithm/bidi_charging.py @@ -100,6 +100,8 @@ def get_missing_currents(self, preferenced_cps: List[Chargepoint], grid_counter) missing_currents = [0, 0, 0] if cp.data.control_parameter.chargemode == Chargemode.INSTANT_CHARGING: # Entladen im Instant-Charging-Modus + if cp.data.set.charging_ev_data.data.get.soc is None: + raise ValueError(f"LP{cp.num}: Auto-Bat SoC unbekannt, daher keine Entladung möglich.") if cp.data.set.charging_ev_data.data.get.soc > 0: # Auto-bat ist nicht leer diff --git a/packages/control/loadmanagement.py b/packages/control/loadmanagement.py index a008b829e0..9f3ad4c75e 100644 --- a/packages/control/loadmanagement.py +++ b/packages/control/loadmanagement.py @@ -133,10 +133,13 @@ def _limit_by_power(self, power_left = max(power_left - feed_in, 0) log.debug(f"Verbleibende Leistung unter Berücksichtigung der Einspeisegrenze: {power_left}W") if abs(total_power) > power_left: + current_sum = sum(available_currents) for i in range(0, 3): try: # Am meisten belastete Phase trägt am meisten zur Leistungsreduktion bei. - currents[i] = available_currents[i] / sum(available_currents) * power_left / cp_voltage + # abs(current_sum) verhindert Vorzeichenwechsel beim Entladen. + currents[i] = available_currents[i] / abs(current_sum) * power_left / cp_voltage + except ZeroDivisionError: # bei einphasig angeschlossenen Wallboxen ist die Spannung der anderen Phasen 0V currents[i] = 0.0 diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 764bfa35df..5a2c06ee5d 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -485,7 +485,8 @@ def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): else: self._validate_value(msg, float, [(float("-inf"), 0), (6, 32), (0, 0)]) elif "/set/required_power" in msg.topic: - self._validate_value(msg, float) + # Don't accept NaN + self._validate_value(msg, float, [(float("-inf"), float("inf"))]) elif "/set/phases_to_use" in msg.topic: self._validate_value(msg, int, [(0, 3)]) elif ("/set/manual_lock" in msg.topic or