diff --git a/packages/control/algorithm/common.py b/packages/control/algorithm/common.py index 02eb1aa0a7..d9dd70be6c 100644 --- a/packages/control/algorithm/common.py +++ b/packages/control/algorithm/common.py @@ -3,7 +3,6 @@ from control import data from control.algorithm.filter_chargepoints import filter_loads_by_chargemodes -from control.algorithm.utils import get_medium_charging_current from control.chargepoint.chargepoint import Chargepoint from control.counter import Counter from control.load import get_load_str @@ -195,16 +194,16 @@ def consider_less_charging_chargepoint_in_loadmanagement(load: Load, set_current expected_current = set_current - 1 expected_current = max(expected_current, 0) if data.data.counter_all_data.data.config.consider_less_charging is False: - if expected_current >= get_medium_charging_current(load.data.get.currents): + if expected_current >= load.get_mean_current(): if load.data.control_parameter.timestamp_charge_start is not None: if check_timestamp(load.data.control_parameter.timestamp_charge_start, LESS_CHARGING_TIMEOUT) is False: log.debug(f"{get_load_str(load)} lädt deutlich unter dem Sollstrom und " f"wird nur mit {load.data.get.currents}A berücksichtigt.") - return get_medium_charging_current(load.data.get.currents) + return load.get_mean_current() elif load.data.set.current_prev != 0: log.debug( f"{get_load_str(load)} lädt nicht und wird nur mit {load.data.get.currents}A berücksichtigt.") - return get_medium_charging_current(load.data.get.currents) + return load.get_mean_current() return set_current # tested diff --git a/packages/control/algorithm/common_test.py b/packages/control/algorithm/common_test.py index ff9a584c20..795e482d73 100644 --- a/packages/control/algorithm/common_test.py +++ b/packages/control/algorithm/common_test.py @@ -155,6 +155,20 @@ def setup_cp(num: int, required_currents) -> Chargepoint: assert currents == expected_currents +@pytest.mark.parametrize("timestamp_charge_start", [None, 1652683152]) +@pytest.mark.parametrize("set_current", [0.25, 2]) +def test_consider_less_charging_keeps_small_consumer_current(timestamp_charge_start, set_current): + consumer = Consumer(1) + consumer.data.get.currents = [0.25, 0, 0] + consumer.data.set.current_prev = 0.25 + consumer.data.control_parameter.timestamp_charge_start = timestamp_charge_start + data.data.counter_all_data.data.config.consider_less_charging = False + + considered = common.consider_less_charging_chargepoint_in_loadmanagement(consumer, set_current) + + assert considered == pytest.approx(0.25) + + @pytest.mark.parametrize( "consider_less_charging, get_currents, expected_considered", [ diff --git a/packages/control/algorithm/surplus_controlled.py b/packages/control/algorithm/surplus_controlled.py index 3c8a48187c..9b36d3b7e5 100644 --- a/packages/control/algorithm/surplus_controlled.py +++ b/packages/control/algorithm/surplus_controlled.py @@ -8,7 +8,6 @@ from control.algorithm.filter_chargepoints import (filter_grouped_loads_by_mode_and_counter, filter_loads_by_chargemodes, filtered_loads_to_str, get_loads_by_chargemodes, get_preferenced_load_charging) -from control.algorithm.utils import get_medium_charging_current from control.chargepoint.charging_type import ChargingType from control.chargepoint.chargepoint import Chargepoint from control.chargepoint.chargepoint_state import ChargepointState, CHARGING_STATES @@ -123,7 +122,7 @@ def _fix_deviating_evse_current(self, chargepoint: Chargepoint) -> float: Wenn die Soll-Stromstärke nicht angepasst worden ist, nicht den ungenutzten EVSE-Strom aufschlagen.""" evse_current = chargepoint.data.get.evse_current if evse_current is not None and chargepoint.data.set.current != chargepoint.data.set.current_prev: - offset = evse_current - get_medium_charging_current(chargepoint.data.get.currents) + offset = evse_current - chargepoint.get_mean_current() offset = min(offset, MAX_EVSE_CURRENT_CHANGE) current_with_offset = chargepoint.data.set.current + offset current = min(current_with_offset, chargepoint.data.control_parameter.required_current) @@ -211,15 +210,15 @@ def limit_adjust_current(load: Load, new_current: float) -> float: else: # Um max. +/- 5A pro Zyklus regeln if (-MAX_CURRENT-nominal_difference - < new_current - get_medium_charging_current(load.data.get.currents) + < new_current - load.get_mean_current() < MAX_CURRENT+nominal_difference): current = new_current else: - if new_current < get_medium_charging_current(load.data.get.currents): - current = get_medium_charging_current(load.data.get.currents) - MAX_CURRENT + if new_current < load.get_mean_current(): + current = load.get_mean_current() - MAX_CURRENT else: - current = get_medium_charging_current(load.data.get.currents) + MAX_CURRENT + current = load.get_mean_current() + MAX_CURRENT load.set_state_and_log(msg) return max(current, load.data.control_parameter.min_current, diff --git a/packages/control/algorithm/surplus_controlled_test.py b/packages/control/algorithm/surplus_controlled_test.py index 0685128353..bf180e5d91 100644 --- a/packages/control/algorithm/surplus_controlled_test.py +++ b/packages/control/algorithm/surplus_controlled_test.py @@ -98,6 +98,16 @@ def test_limit_adjust_current(new_current: float, expected_current: float, monke assert current == expected_current +@pytest.mark.parametrize("new_current, expected_current", [(0.3, 0.3), (6, 5.25)]) +def test_limit_adjust_current_uses_small_consumer_current(new_current, expected_current): + consumer = Consumer(1) + consumer.data.get.currents = [0.25, 0, 0] + consumer.data.get.charge_state = True + consumer.data.control_parameter.min_current = 0.25 + + assert limit_adjust_current(consumer, new_current) == pytest.approx(expected_current) + + @pytest.mark.parametrize("phases, required_currents, expected_currents", [ pytest.param(1, [10, 0, 0], [16, 0, 0]), diff --git a/packages/control/algorithm/utils.py b/packages/control/algorithm/utils.py index 562662b17d..fec99b3cc5 100644 --- a/packages/control/algorithm/utils.py +++ b/packages/control/algorithm/utils.py @@ -1,10 +1,10 @@ from typing import List -def get_medium_charging_current(currents: List[float]) -> float: +def get_medium_charging_current(currents: List[float], threshold: float = 0.5) -> float: """Ermittelt den mittleren Ladestrom der Phasen, auf denen geladen wird. """ - if any(abs(x) >= 0.5 for x in currents): - return sum(x for x in currents if abs(x) >= 0.5) / len([x for x in currents if abs(x) >= 0.5]) + if any(abs(x) >= threshold for x in currents): + return sum(x for x in currents if abs(x) >= threshold) / len([x for x in currents if abs(x) >= threshold]) else: return 0.0 diff --git a/packages/control/algorithm/utils_test.py b/packages/control/algorithm/utils_test.py new file mode 100644 index 0000000000..331855bca7 --- /dev/null +++ b/packages/control/algorithm/utils_test.py @@ -0,0 +1,17 @@ +import pytest + +from control.algorithm.utils import get_medium_charging_current + + +@pytest.mark.parametrize( + "currents, expected_current", + [ + ([0.25, 0, 0], 0), + ([0.5, 0, 0], 0.5), + ([0.25, 0.75, 0], 0.75), + ([6, 8, 0], 7), + ([0, 0, 0], 0), + ], +) +def test_charging_current_keeps_default_chargepoint_threshold(currents, expected_current): + assert get_medium_charging_current(currents) == pytest.approx(expected_current) diff --git a/packages/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index 58f0934b4e..47de2f458f 100644 --- a/packages/control/chargepoint/chargepoint.py +++ b/packages/control/chargepoint/chargepoint.py @@ -4,7 +4,6 @@ from typing import Dict, Optional, Tuple from fnmatch import fnmatch -from control.algorithm.utils import get_medium_charging_current from control.chargelog import chargelog from control import data from control.chargemode import Chargemode @@ -695,7 +694,7 @@ def update(self, ev_list: Dict[str, Ev]) -> None: f"{self.data.set.charge_template.data.chargemode.selected}, Submodus: " f"{self.data.control_parameter.submode}, Phasen: " f"{self.data.control_parameter.phases}" - f", mittlerer Ist-Strom: {get_medium_charging_current(self.data.get.currents)}") + f", mittlerer Ist-Strom: {self.get_mean_current()}") except Exception: log.exception("Fehler im Prepare-Modul für Ladepunkt "+str(self.num)) self.data.control_parameter.submode = Chargemode.STOP diff --git a/packages/control/consumer/consumer.py b/packages/control/consumer/consumer.py index baff6e5e61..bd605f957d 100644 --- a/packages/control/consumer/consumer.py +++ b/packages/control/consumer/consumer.py @@ -4,7 +4,6 @@ from control import data from control.consumer.usage import NOT_CONTROLLED, ON_OFF -from control.algorithm.utils import get_medium_charging_current from control.chargemode import Chargemode from control.chargepoint.chargepoint_state import CHARGING_STATES, ChargepointState from control.consumer.consumer_data import ConsumerData, ConsumerUsage, WaitForStartStates @@ -83,7 +82,7 @@ def _get_min_current(self) -> float: return self._convert_power_to_current(self.data.config.min_power) else: if self.data.get.charge_state: - return get_medium_charging_current(self.data.get.currents) + return self.get_mean_current() else: return self._convert_power_to_current(self.data.config.max_power) @@ -159,7 +158,7 @@ def get_parameter(self) -> Tuple[float, Optional[str], Optional[Chargemode], Cha def _parse_required_current_by_usage(self, required_current: float) -> float: if self.data.usage.type in ON_OFF: if self.data.get.charge_state: - return get_medium_charging_current(self.data.get.currents) + return self.get_mean_current() else: return self._convert_power_to_current(self.data.config.max_power) else: diff --git a/packages/control/consumer/consumer_test.py b/packages/control/consumer/consumer_test.py index b08cfa0f8a..97789be13f 100644 --- a/packages/control/consumer/consumer_test.py +++ b/packages/control/consumer/consumer_test.py @@ -90,6 +90,40 @@ def test_set_plug_state_for_controlled_consumers( assert consumer.data.get.plug_state is expected_plug_state +@pytest.mark.parametrize("usage_type", [ConsumerUsage.CONTINUOUS, ConsumerUsage.SUSPENDABLE_ONOFF]) +@pytest.mark.parametrize("chargemode", [Chargemode.INSTANT_CHARGING, Chargemode.PV_CHARGING]) +@pytest.mark.parametrize( + "currents, expected_current", + [ + pytest.param([0.25, 0, 0], 0.25, id="shelly-small-load"), + pytest.param([0, 0.16, 0], 0.16, id="just-above-standby"), + pytest.param([0.25, 0.35, 0], 0.3, id="two-small-phases"), + pytest.param([0.25, 0.75, 0], 0.5, id="mixed-small-and-large-phases"), + pytest.param([1, 0, 0], 1, id="larger-load"), + ], +) +def test_running_onoff_consumer_keeps_small_load_enabled( + consumer: Consumer, + usage_type: ConsumerUsage, + chargemode: Chargemode, + currents: List[float], + expected_current: float): + consumer.data.usage.type = usage_type + consumer.data.usage.chargemode = chargemode + consumer.data.get.currents = currents + consumer.data.get.charge_state = True + + # Wiederholte Regelungen nach Ablauf des Mindestintervalls dürfen den + # laufenden Verbraucher nicht wegen der 0,5-A-Ladepunktschwelle abschalten. + for _ in range(3): + consumer.data.set.switch_interval_elapsed = True + required_current, _, mode, submode = consumer.get_parameter() + assert required_current == pytest.approx(expected_current) + assert consumer._get_min_current() == pytest.approx(expected_current) + assert mode == chargemode + assert submode == chargemode + + def test_get_parameter_clears_previous_state_string_when_switch_interval_is_active(consumer: Consumer): # setup consumer.data.set.switch_interval_elapsed = False diff --git a/packages/control/counter.py b/packages/control/counter.py index 6446cda804..e4cd0446af 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -11,7 +11,6 @@ from control.chargepoint.chargepoint_state import ChargepointState from control.chargepoint.chargepoint import Chargepoint from control.chargemode import Chargemode -from control.algorithm.utils import get_medium_charging_current from control.algorithm.filter_chargepoints import get_loads_by_chargemodes from dataclasses import dataclass, field from enum import Enum @@ -210,7 +209,7 @@ def _set_current_left(self, loadmanagement_available: bool) -> None: load.data.config.phase_1, load.data.get.currents) except KeyError: - element_current = [get_medium_charging_current(load.data.get.currents)]*3 + element_current = [load.get_mean_current()]*3 # Vorzeichen anhand der Gesamtleistung entscheiden, nicht anhand einzelner Phasenströme # (min(element_current) < 0): bei ein-/zweiphasigem Laden zeigen ungenutzte Phasen oft ein # geringes negatives Messrauschen (zB -0.05A), das sonst den ganzen Load faelschlich als @@ -632,7 +631,7 @@ def switch_off_check_threshold(self, load: Load) -> bool: # und wieder eingespeist wird. 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)): + load.get_mean_current() <= min_current)): if 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 diff --git a/packages/control/counter_test.py b/packages/control/counter_test.py index b8550a815a..e0b34d5c61 100644 --- a/packages/control/counter_test.py +++ b/packages/control/counter_test.py @@ -135,6 +135,24 @@ def test_set_current_left_nets_out_own_current_despite_noise_on_unused_phase(mon assert counter.data.set.raw_currents_left == [45, 40.05, 37.95] +def test_set_current_left_accounts_for_small_consumer_with_unknown_phase(monkeypatch, data_): + consumer = Consumer(7) + consumer.data.config.phase_1 = 0 + consumer.data.get.currents = [0.25, 0, 0] + consumer.data.get.power = 57.5 + consumer.data.set.switch_interval_elapsed = True + data.data.consumer_data = {"consumer7": consumer} + monkeypatch.setattr(data.data.counter_all_data, "get_loads_of_counter", Mock(return_value=["consumer7"])) + counter = Counter(0) + counter.data.config.max_currents = [16]*3 + counter.data.config.max_total_power = 16*3*230 + counter.data.get.currents = [1]*3 + + counter._set_current_left(True) + + assert counter.data.set.raw_currents_left == pytest.approx([15.25]*3) + + @dataclass class Params: name: str diff --git a/packages/control/load_protocol.py b/packages/control/load_protocol.py index f53807574b..8b0c50c489 100644 --- a/packages/control/load_protocol.py +++ b/packages/control/load_protocol.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from typing import List, Optional, Protocol +from control.algorithm.utils import get_medium_charging_current from control.chargepoint.control_parameter import ControlParameter from helpermodules.timecheck import create_timestamp @@ -42,11 +43,17 @@ class LoadData: class Load(Protocol): + STANDBY_THRESHOLD = 0.5 + num: int chargemode_changed: bool submode_changed: bool data: LoadData + def get_mean_current(self) -> float: + """Mittlerer Ist-Strom aktiver Phasen mit der Schwelle des jeweiligen Lasttyps.""" + return get_medium_charging_current(self.data.get.currents, threshold=self.STANDBY_THRESHOLD) + def set_timestamp_charge_start(self) -> None: # Beim Ladestart Timer laufen lassen, manche Fahrzeuge brauchen sehr lange. # Nach dem Algorithmus setzen, sonst steht set current noch nicht fest. diff --git a/packages/control/load_protocol_test.py b/packages/control/load_protocol_test.py new file mode 100644 index 0000000000..1a67b0e4b2 --- /dev/null +++ b/packages/control/load_protocol_test.py @@ -0,0 +1,25 @@ +import pytest + +from control.chargepoint.chargepoint import Chargepoint +from control.consumer.consumer import Consumer + + +@pytest.mark.parametrize( + "currents, expected_consumer, expected_chargepoint", + [ + ([0, 0, 0], 0, 0), + ([0.1, 0, 0], 0, 0), + ([0.25, 0, 0], 0.25, 0), + ([0.25, 0.35, 0.05], 0.3, 0), + ([0.25, 0.75, 0], 0.5, 0.75), + ([6, 8, 0.05], 7, 7), + ], +) +def test_mean_current_uses_load_specific_standby_threshold(currents, expected_consumer, expected_chargepoint): + consumer = Consumer(1) + chargepoint = Chargepoint(2, None) + consumer.data.get.currents = currents + chargepoint.data.get.currents = currents + + assert consumer.get_mean_current() == pytest.approx(expected_consumer) + assert chargepoint.get_mean_current() == pytest.approx(expected_chargepoint)