Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions packages/control/algorithm/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions packages/control/algorithm/common_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
[
Expand Down
11 changes: 5 additions & 6 deletions packages/control/algorithm/surplus_controlled.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions packages/control/algorithm/surplus_controlled_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand Down
6 changes: 3 additions & 3 deletions packages/control/algorithm/utils.py
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions packages/control/algorithm/utils_test.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 1 addition & 2 deletions packages/control/chargepoint/chargepoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions packages/control/consumer/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down
34 changes: 34 additions & 0 deletions packages/control/consumer/consumer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions packages/control/counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions packages/control/counter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions packages/control/load_protocol.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.
Expand Down
25 changes: 25 additions & 0 deletions packages/control/load_protocol_test.py
Original file line number Diff line number Diff line change
@@ -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)
Loading