diff --git a/data/config/mosquitto/openwb_local.conf b/data/config/mosquitto/openwb_local.conf index f540b3dcf1..8d37590972 100644 --- a/data/config/mosquitto/openwb_local.conf +++ b/data/config/mosquitto/openwb_local.conf @@ -1,4 +1,4 @@ -# openwb-version:19 +# openwb-version:20 listener 1886 localhost allow_anonymous true @@ -52,7 +52,7 @@ topic openWB/optional/# out 2 topic openWB/counter/config/# out 2 topic openWB/counter/set/# out 2 -topic openWB/counter/get/hierarchy out 2 +topic openWB/counter/get/# out 2 topic openWB/counter/+/module/# out 2 topic openWB/counter/+/config/# out 2 topic openWB/counter/+/get/# out 2 diff --git a/packages/control/algorithm/additional_current.py b/packages/control/algorithm/additional_current.py index 1cc76de58e..4ae9519563 100644 --- a/packages/control/algorithm/additional_current.py +++ b/packages/control/algorithm/additional_current.py @@ -1,11 +1,12 @@ import logging +from typing import List from control.algorithm import common from control.algorithm.chargemodes import CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT from control.limiting_value import LoadmanagementLimit from control.loadmanagement import Loadmanagement from control.chargepoint.chargepoint import Chargepoint -from control.algorithm.filter_chargepoints import (get_chargepoints_by_mode_and_counter, +from control.algorithm.filter_chargepoints import (get_chargepoints_by_mode_and_counter_and_lm_prio, get_preferenced_chargepoint_charging) log = logging.getLogger(__name__) @@ -16,14 +17,17 @@ class AdditionalCurrent: def __init__(self) -> None: pass - def set_additional_current(self) -> None: - common.reset_current_by_chargemode(CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT) - for mode_tuple, counter in common.mode_and_counter_generator(CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT): + def set_additional_current(self, cp_prio_group: List[Chargepoint]) -> None: + log.info("**Soll-Strom setzen**") + common.reset_current_to_target_current(cp_prio_group) + common.reset_current_by_chargemode(CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT, cp_prio_group) + for counter in common.counter_generator(): preferenced_chargepoints, preferenced_cps_without_set_current = get_preferenced_chargepoint_charging( - get_chargepoints_by_mode_and_counter(mode_tuple, f"counter{counter.num}")) + get_chargepoints_by_mode_and_counter_and_lm_prio(CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT, + f"counter{counter.num}", cp_prio_group)) if preferenced_chargepoints: common.update_raw_data(preferenced_chargepoints) - log.info(f"Mode-Tuple {mode_tuple[0]} - {mode_tuple[1]} - {mode_tuple[2]}, Zähler {counter.num}") + log.info(f"Zähler {counter.num}, Verbraucher {[f'LP{cp.num}' for cp in preferenced_chargepoints]}") while len(preferenced_chargepoints): cp = preferenced_chargepoints[0] missing_currents, counts = common.get_missing_currents_left(preferenced_chargepoints) diff --git a/packages/control/algorithm/algorithm.py b/packages/control/algorithm/algorithm.py index 73656d2524..4f07f559f5 100644 --- a/packages/control/algorithm/algorithm.py +++ b/packages/control/algorithm/algorithm.py @@ -1,6 +1,6 @@ import logging +from typing import List -from control import counter from control import data from control.algorithm import common from control.algorithm.additional_current import AdditionalCurrent @@ -8,6 +8,7 @@ from control.algorithm.min_current import MinCurrent from control.algorithm.no_current import NoCurrent from control.algorithm.surplus_controlled import SurplusControlled +from control.chargepoint.chargepoint import Chargepoint log = logging.getLogger(__name__) @@ -23,39 +24,35 @@ def __init__(self): def calc_current(self) -> None: """ Einstiegspunkt in den Regel-Algorithmus """ - try: - log.info("# Algorithmus") - self.evu_counter = data.data.counter_all_data.get_evu_counter() - self._check_auto_phase_switch_delay() - self.surplus_controlled.check_submode_pv_charging() - common.reset_current() - log.info("**Mindestrom setzen**") - self.min_current.set_min_current() - log.info("**Soll-Strom setzen**") - common.reset_current_to_target_current() - self.additional_current.set_additional_current() - self.surplus_controlled.set_required_current_to_max() - log.info("**PV-geführten Strom setzen**") - counter.limit_raw_power_left_to_surplus(self.evu_counter.calc_raw_surplus()) - if self.evu_counter.data.set.surplus_power_left > 0: - common.reset_current_to_target_current() - self.surplus_controlled.set_surplus_current() - else: - log.info("Keine Leistung für PV-geführtes Laden übrig.") - log.info("**Bidi-(Ent-)Lade-Strom setzen**") - counter.set_raw_surplus_power_left() - self.bidi.set_bidi() - self.no_current.set_no_current() - self.no_current.set_none_current() - except Exception: - log.exception("Fehler im Algorithmus-Modul") + log.info("# Algorithmus") + common.reset_current() + for next_low_power_group, next_full_power_group in data.data.counter_all_data.prio_groups_generator(): + try: + if next_low_power_group is not None: + self._check_auto_phase_switch_delay(next_low_power_group) + self.surplus_controlled.check_submode_pv_charging(next_low_power_group) + self.min_current.set_min_current(next_low_power_group) + if next_full_power_group is not None: + self._check_auto_phase_switch_delay(next_full_power_group) + self.surplus_controlled.check_submode_pv_charging(next_full_power_group) + self.min_current.set_min_current(next_full_power_group) + self.additional_current.set_additional_current(next_full_power_group) + self.surplus_controlled.set_surplus_current(next_full_power_group) + if next_low_power_group is not None: + self.additional_current.set_additional_current(next_low_power_group) + self.surplus_controlled.set_surplus_current(next_low_power_group) + except Exception: + log.exception("Fehler im Algorithmus-Modul") + self.bidi.set_bidi() + self.no_current.set_no_current() + self.no_current.set_none_current() - def _check_auto_phase_switch_delay(self) -> None: + def _check_auto_phase_switch_delay(self, cps: List[Chargepoint]) -> None: """ geht alle LP durch und prüft, ob eine Ladung aktiv ist, ob automatische Phasenumschaltung möglich ist und ob ob ein Timer gestartet oder gestoppt werden muss oder ob ein Timer abgelaufen ist. """ - for cp in data.data.cp_data.values(): + for cp in cps: try: if cp.data.control_parameter.required_current != 0: charging_ev = cp.data.set.charging_ev_data diff --git a/packages/control/algorithm/bidi_charging.py b/packages/control/algorithm/bidi_charging.py index 52161874c9..35f0311910 100644 --- a/packages/control/algorithm/bidi_charging.py +++ b/packages/control/algorithm/bidi_charging.py @@ -2,6 +2,7 @@ from control import data from control.algorithm.chargemodes import CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE from control.algorithm.filter_chargepoints import get_chargepoints_by_mode +from control.counter import set_raw_surplus_power_left from helpermodules.phase_handling import voltages_mean log = logging.getLogger(__name__) @@ -12,35 +13,37 @@ def __init__(self): pass def set_bidi(self): + log.info("**Bidi-(Ent-)Lade-Strom setzen**") + set_raw_surplus_power_left() 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_by_mode(mode_tuple) - if preferenced_cps: - log.info( - 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] - else: + preferenced_cps = get_chargepoints_by_mode(CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE) + if preferenced_cps: + log.info(f"Verbraucher {preferenced_cps}") + 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] = 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") - preferenced_cps.pop(0) + 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)) + grid_counter.update_values_left(missing_currents, voltages_mean(cp.data.get.voltages)) + grid_counter.update_currents_left(missing_currents) + cp.data.set.current = missing_currents[0] + log.info(f"LP{cp.num}: Stromstärke {missing_currents}A") + preferenced_cps.pop(0) diff --git a/packages/control/algorithm/chargemodes.py b/packages/control/algorithm/chargemodes.py index 0ebfa07cf3..5dbbd23673 100644 --- a/packages/control/algorithm/chargemodes.py +++ b/packages/control/algorithm/chargemodes.py @@ -2,32 +2,22 @@ # 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), + (None, Chargemode.TIME_CHARGING), + (Chargemode.INSTANT_CHARGING, Chargemode.INSTANT_CHARGING), + (Chargemode.ECO_CHARGING, Chargemode.INSTANT_CHARGING), + (Chargemode.PV_CHARGING, Chargemode.INSTANT_CHARGING), + (Chargemode.SCHEDULED_CHARGING, Chargemode.PV_CHARGING), + (Chargemode.ECO_CHARGING, Chargemode.PV_CHARGING), + (Chargemode.PV_CHARGING, Chargemode.PV_CHARGING), # 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), + (None, Chargemode.STOP),) -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_CHARGING = CHARGEMODES[0:16] +CONSIDERED_CHARGE_MODES_SURPLUS = (CHARGEMODES[0], *CHARGEMODES[3:8]) +CONSIDERED_CHARGE_MODES_PV_ONLY = CHARGEMODES[5:8] +CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT = CHARGEMODES[0:5] +CONSIDERED_CHARGE_MODES_MIN_CURRENT = CHARGEMODES[0:-2] +CONSIDERED_CHARGE_MODES_NO_CURRENT = (CHARGEMODES[9],) +CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE = (CHARGEMODES[8],) +CONSIDERED_CHARGE_MODES_CHARGING = CHARGEMODES[0:8] diff --git a/packages/control/algorithm/common.py b/packages/control/algorithm/common.py index 11a345c128..533f90a273 100644 --- a/packages/control/algorithm/common.py +++ b/packages/control/algorithm/common.py @@ -2,7 +2,7 @@ from typing import Iterable, List, Optional, Tuple from control import data -from control.algorithm.filter_chargepoints import get_chargepoints_by_mode +from control.algorithm.filter_chargepoints import get_chargepoints_by_mode_and_lm_prio from control.algorithm.utils import get_medium_charging_current from control.chargepoint.chargepoint import Chargepoint from control.counter import Counter @@ -27,20 +27,19 @@ def reset_current(): log.exception(f"Fehler im Algorithmus-Modul für Ladepunkt{cp.num}") -def reset_current_by_chargemode(mode_tuple: Tuple[Optional[str], str, bool]) -> None: - for mode in mode_tuple: - for cp in get_chargepoints_by_mode(mode): - cp.data.set.current = None +def reset_current_by_chargemode(chargemodes: Tuple[Tuple[Optional[str], str]], + cp_prio_group: List[Chargepoint]) -> None: + for cp in get_chargepoints_by_mode_and_lm_prio(chargemodes, cp_prio_group): + cp.data.set.current = None -def mode_and_counter_generator(chargemodes: List) -> Iterable[Tuple[Tuple[Optional[str], str, bool], Counter]]: - for mode_tuple in chargemodes: - levels = data.data.counter_all_data.get_list_of_elements_per_level() - for level in reversed(levels): - for element in level: - if element["type"] == ComponentType.COUNTER.value: - counter = data.data.counter_data[f"counter{element['id']}"] - yield mode_tuple, counter +def counter_generator() -> Iterable[Counter]: + levels = data.data.counter_all_data.get_list_of_elements_per_level() + for level in reversed(levels): + for element in level: + if element["type"] == ComponentType.COUNTER.value: + counter = data.data.counter_data[f"counter{element['id']}"] + yield counter # tested @@ -78,10 +77,10 @@ def set_current_counterdiff(diff_current: float, 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.counter_data[counter].update_values_left( + diffs, + voltages_mean(chargepoint.data.get.voltages)) + data.data.counter_data[counter].update_currents_left(diffs) data.data.io_actions.dimming_set_import_power_left({"type": "cp", "id": chargepoint.num}, sum(diffs)*230) chargepoint.data.set.current = current @@ -112,10 +111,12 @@ def available_current_for_cp(chargepoint: Chargepoint, control_parameter = chargepoint.data.control_parameter available_current = float("inf") missing_current_cp = control_parameter.required_current - chargepoint.data.set.target_current + for i in range(0, 3): - if (control_parameter.required_currents[i] != 0 and - missing_currents[i] != available_currents[i]): - available_current = min(min(missing_current_cp, available_currents[i]/counts[i]), available_current) + shared_with = 1 if chargepoint.data.set.charging_ev_data.data.full_power else counts[i] + if (control_parameter.required_currents[i] != 0 and missing_currents[i] != available_currents[i]): + available_current = min(min(missing_current_cp, available_currents[i]/shared_with), available_current) + if available_current == float("inf"): available_current = missing_current_cp return available_current @@ -154,12 +155,12 @@ def update_raw_data(preferenced_chargepoints: List[Chargepoint], 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.counter_data[counter].update_values_left(diffs, voltages_mean(chargepoint.data.get.voltages)) + data.data.counter_data[counter].update_currents_left(diffs) data.data.io_actions.dimming_set_import_power_left({"type": "cp", "id": chargepoint.num}, sum(diffs)*230) -def consider_less_charging_chargepoint_in_loadmanagement(cp: Chargepoint, set_current: float) -> bool: +def consider_less_charging_chargepoint_in_loadmanagement(cp: Chargepoint, set_current: float) -> float: if (data.data.counter_all_data.data.config.consider_less_charging is False and ((set_current - cp.data.set.charging_ev_data.ev_template.data.nominal_difference) > get_medium_charging_current( @@ -191,12 +192,13 @@ def get_missing_currents_left(preferenced_chargepoints: List[Chargepoint]) -> Tu return missing_currents, counts -def reset_current_to_target_current(): +def reset_current_to_target_current(cp_prio_group: List[Chargepoint]) -> None: """target_current enthält die gesetzte Stromstärke der vorherigen Stufe. Notwendig, um zB bei der Mindeststromstärke erkennen zu können, ob diese ein vom LM begrenzter Strom aus Stufe 2 oder der Mindeststrom aus Stufe 1 ist.""" - for cp in data.data.cp_data.values(): + for cp in cp_prio_group: try: - cp.data.set.target_current = cp.data.set.current + if cp.data.set.current is not None: + cp.data.set.target_current = cp.data.set.current except Exception: log.exception(f"Fehler im Algorithmus-Modul für Ladepunkt{cp.num}") diff --git a/packages/control/algorithm/filter_chargepoints.py b/packages/control/algorithm/filter_chargepoints.py index 7af2f319f3..2b3e6cd691 100644 --- a/packages/control/algorithm/filter_chargepoints.py +++ b/packages/control/algorithm/filter_chargepoints.py @@ -8,38 +8,42 @@ log = logging.getLogger(__name__) -def get_chargepoints_by_mode_and_counter(mode_tuple: Tuple[Optional[str], str, bool], - counter: str) -> List[Chargepoint]: +def get_chargepoints_by_mode_and_counter_and_lm_prio(chargemodes: Tuple[Tuple[Optional[str], str]], + counter: str, + prio_group: List[Chargepoint]) -> List[Chargepoint]: cps_to_counter = data.data.counter_all_data.get_chargepoints_of_counter(counter) cps_to_counter_ids = [int(cp[2:]) for cp in cps_to_counter] - cps_by_mode = get_chargepoints_by_mode(mode_tuple) - return list(filter(lambda cp: cp.num in cps_to_counter_ids, cps_by_mode)) + cps_by_mode = get_chargepoints_by_mode(chargemodes) + return [ + cp for cp in prio_group + if cp in cps_by_mode and cp.num in cps_to_counter_ids + ] # tested -def get_chargepoints_by_mode(mode_tuple: Tuple[Optional[str], str, bool]) -> List[Chargepoint]: - mode = mode_tuple[0] - submode = mode_tuple[1] - prio = mode_tuple[2] - # enthält alle LP, auf die das Tupel zutrifft - valid_chargepoints = [] - for cp in data.data.cp_data.values(): - if cp.data.control_parameter.required_current != 0: - if ((cp.data.control_parameter.prio == prio) and - (cp.data.control_parameter.chargemode == mode or - mode is None) and - (cp.data.control_parameter.submode == submode)): - valid_chargepoints.append(cp) - return valid_chargepoints +def get_chargepoints_by_mode(chargemodes: Tuple[Tuple[Optional[str], str]]) -> List[Chargepoint]: + cps = [] + for chargemode in chargemodes: + for cp in data.data.cp_data.values(): + if (cp.data.control_parameter.required_current != 0 and + (cp.data.control_parameter.chargemode == chargemode[0] or chargemode[0] is None) and + (cp.data.control_parameter.submode == chargemode[1])): + cps.append(cp) + return cps + + +def get_chargepoints_by_mode_and_lm_prio(chargemodes: Tuple[Tuple[Optional[str], str]], + prio_group: List[Chargepoint]) -> List[Chargepoint]: + cps_by_mode = get_chargepoints_by_mode(chargemodes) + return [cp for cp in prio_group if cp in cps_by_mode] def get_preferenced_chargepoint_charging( chargepoints: List[Chargepoint]) -> Tuple[List[Chargepoint], List[Chargepoint]]: - preferenced_chargepoints = _get_preferenced_chargepoint(chargepoints) preferenced_chargepoints_with_set_current = [] preferenced_chargepoints_without_set_current = [] - for cp in preferenced_chargepoints: + for cp in chargepoints: if cp.data.set.target_current == 0: log.info( f"LP {cp.num}: Keine Zuteilung des Mindeststroms, daher keine weitere Berücksichtigung") @@ -52,7 +56,6 @@ def get_preferenced_chargepoint_charging( preferenced_chargepoints_with_set_current.append(cp) return preferenced_chargepoints_with_set_current, preferenced_chargepoints_without_set_current - # tested @@ -104,10 +107,3 @@ def _get_preferenced_chargepoint(valid_chargepoints: List[Chargepoint]) -> List: except Exception: log.exception("Fehler im Algorithmus-Modul") return preferenced_chargepoints - - -def get_chargepoints_by_chargemodes(modes) -> List[Chargepoint]: - chargepoints: List[Chargepoint] = [] - for mode in modes: - chargepoints.extend(get_chargepoints_by_mode(mode)) - return chargepoints diff --git a/packages/control/algorithm/filter_chargepoints_test.py b/packages/control/algorithm/filter_chargepoints_test.py index a8b74dc8b0..7996a1b3a1 100644 --- a/packages/control/algorithm/filter_chargepoints_test.py +++ b/packages/control/algorithm/filter_chargepoints_test.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Optional, Tuple +from typing import List from unittest.mock import Mock import pytest @@ -14,6 +14,11 @@ from control.ev.ev import Ev, EvData, Get +@pytest.fixture() +def mock_data() -> None: + data.data_init(Mock()) + + @dataclass class PreferencedParams: name: str @@ -96,62 +101,53 @@ def mock_cp(cp: Chargepoint, num: int): @pytest.mark.parametrize( - "set_mode_tuple, required_current_1, mode_tuple_1, mode_tuple_2, expected_valid_chargepoints", + "required_current_1, expected_cp_indices", [ - pytest.param((Chargemode.SCHEDULED_CHARGING, Chargemode.INSTANT_CHARGING, False), - 6, (Chargemode.SCHEDULED_CHARGING, - Chargemode.INSTANT_CHARGING, False), - (Chargemode.SCHEDULED_CHARGING, - Chargemode.INSTANT_CHARGING, False), - [mock_cp1, mock_cp2], id="fits mode"), - pytest.param((Chargemode.SCHEDULED_CHARGING, Chargemode.INSTANT_CHARGING, False), - 0, (Chargemode.SCHEDULED_CHARGING, - Chargemode.INSTANT_CHARGING, False), - (Chargemode.SCHEDULED_CHARGING, - Chargemode.INSTANT_CHARGING, False), - [mock_cp2], id="cp1 should not charge"), - pytest.param((Chargemode.SCHEDULED_CHARGING, Chargemode.INSTANT_CHARGING, False), - 6, (Chargemode.SCHEDULED_CHARGING, - Chargemode.INSTANT_CHARGING, False), - (Chargemode.SCHEDULED_CHARGING, - Chargemode.INSTANT_CHARGING, True), - [mock_cp1], id="cp2 is prioritized") + pytest.param(6, [1, 2], id="fits mode"), + pytest.param(0, [2], id="cp1 should not charge"), ]) -def test_get_chargepoints_by_mode(set_mode_tuple: Tuple[Optional[str], str, bool], - required_current_1: int, - mode_tuple_1: Tuple[str, str, bool], - mode_tuple_2: Tuple[str, str, bool], - expected_valid_chargepoints): +def test_get_chargepoints_by_mode(required_current_1: int, + expected_cp_indices, + mock_cp1, mock_cp2): # setup - def setup_cp(cp: Chargepoint, required_current: float, mode_tuple: Tuple[str, str, bool]) -> Chargepoint: + def setup_cp(cp: Chargepoint, required_current: float) -> Chargepoint: + cp.data.set.charging_ev_data = Ev(cp.num) + cp.data.config.ev = cp.num cp.data.control_parameter.required_current = required_current - cp.data.control_parameter.prio = mode_tuple[2] - cp.data.control_parameter.chargemode = mode_tuple[0] - cp.data.control_parameter.submode = mode_tuple[1] + cp.data.control_parameter.chargemode = Chargemode.SCHEDULED_CHARGING + cp.data.control_parameter.submode = Chargemode.INSTANT_CHARGING + cp.data.set.plug_time = 1 return cp - data.data.cp_data = {"cp1": setup_cp(mock_cp1, required_current_1, mode_tuple_1), - "cp2": setup_cp(mock_cp2, 6, mode_tuple_2)} + data.data.cp_data = {"cp1": setup_cp(mock_cp1, required_current_1), + "cp2": setup_cp(mock_cp2, 6)} # evaluation - valid_chargepoints = filter_chargepoints.get_chargepoints_by_mode(set_mode_tuple) + valid_chargepoints = filter_chargepoints.get_chargepoints_by_mode( + ((Chargemode.SCHEDULED_CHARGING, Chargemode.INSTANT_CHARGING),)) # assertion + cp_mapping = {1: mock_cp1, 2: mock_cp2} + expected_valid_chargepoints = [cp_mapping[i] for i in expected_cp_indices] assert valid_chargepoints == expected_valid_chargepoints @pytest.mark.parametrize( - "chargepoints_of_counter, chargepoints_by_mode, expected_chargepoints", + "chargepoints_of_counter, chargepoints_by_mode_indices, expected_cp_indices", [ - pytest.param(["cp1", "cp2"], [mock_cp1, mock_cp2], [mock_cp1, mock_cp2], id="match all"), - pytest.param(["cp1", "cp2"], [mock_cp1], [mock_cp1], id="match by mode"), - pytest.param(["cp2"], [mock_cp1, mock_cp2], [mock_cp2], id="match by counter"), - pytest.param(["cp1"], [mock_cp2], [], id="match none") + pytest.param(["cp1", "cp2"], [1, 2], [1, 2], id="match all"), + pytest.param(["cp1", "cp2"], [1], [1], id="match by mode"), + pytest.param(["cp2"], [1, 2], [2], id="match by counter"), + pytest.param(["cp1"], [2], [], id="match none") ]) def test_get_chargepoints_by_mode_and_counter(chargepoints_of_counter: List[str], - chargepoints_by_mode: List[Chargepoint], - expected_chargepoints: List[Chargepoint], - monkeypatch): + chargepoints_by_mode_indices: List[int], + expected_cp_indices: List[int], + monkeypatch, mock_cp1, mock_cp2, mock_data): # setup + cp_mapping = {1: mock_cp1, 2: mock_cp2} + chargepoints_by_mode = [cp_mapping[i] for i in chargepoints_by_mode_indices] + expected_chargepoints = [cp_mapping[i] for i in expected_cp_indices] + get_chargepoints_of_counter_mock = Mock(return_value=chargepoints_of_counter) monkeypatch.setattr(CounterAll, "get_chargepoints_of_counter", get_chargepoints_of_counter_mock) get_chargepoints_by_mode_mock = Mock(return_value=chargepoints_by_mode) @@ -159,7 +155,8 @@ def test_get_chargepoints_by_mode_and_counter(chargepoints_of_counter: List[str] data.data.counter_all_data = CounterAll() # evaluation - valid_chargepoints = filter_chargepoints.get_chargepoints_by_mode_and_counter(Mock(), "counter6") + valid_chargepoints = filter_chargepoints.get_chargepoints_by_mode_and_counter_and_lm_prio(Mock(), "counter6", ( + mock_cp1, mock_cp2)) # assertion assert valid_chargepoints == expected_chargepoints diff --git a/packages/control/algorithm/integration_test/conftest.py b/packages/control/algorithm/integration_test/conftest.py index dd91361d37..7e731ed347 100644 --- a/packages/control/algorithm/integration_test/conftest.py +++ b/packages/control/algorithm/integration_test/conftest.py @@ -27,6 +27,7 @@ def data_() -> None: for i in range(3, 6): data.data.cp_data[f"cp{i}"].template = CpTemplate() data.data.cp_data[f"cp{i}"].data.config.phase_1 = i-2 + data.data.cp_data[f"cp{i}"].data.config.ev = 0 data.data.cp_data[f"cp{i}"].data.set.charging_ev_data = Ev(i) data.data.cp_data[f"cp{i}"].data.set.charging_ev_data.ev_template.data.max_current_single_phase = 32 data.data.cp_data[f"cp{i}"].data.get.plug_state = True @@ -49,6 +50,8 @@ def data_() -> None: data.data.counter_data["counter6"].data.config.max_total_power = 11000 data.data.counter_all_data = CounterAll() data.data.counter_all_data.data.get.hierarchy = NESTED_HIERARCHY + data.data.counter_all_data.data.get.loadmanagement_prios = [{"type": "vehicle", "id": 0}] + data.data.ev_data["ev0"] = Ev(0) data.data.counter_all_data.data.config.consider_less_charging = True data.data.io_actions = IoActions() diff --git a/packages/control/algorithm/integration_test/instant_charging_test.py b/packages/control/algorithm/integration_test/instant_charging_test.py index afc9b23551..c0a857ba23 100644 --- a/packages/control/algorithm/integration_test/instant_charging_test.py +++ b/packages/control/algorithm/integration_test/instant_charging_test.py @@ -7,6 +7,7 @@ from control.chargemode import Chargemode from control import data, loadmanagement from control.algorithm.algorithm import Algorithm +from control.ev.ev import Ev from control.limiting_value import LimitingValue from dataclass_utils.factories import currents_list_factory @@ -43,6 +44,13 @@ def all_cp_instant_charging_3p(): control_parameter.chargemode = Chargemode.INSTANT_CHARGING data.data.cp_data[f"cp{i}"].data.get.charge_state = True data.data.cp_data[f"cp{i}"].data.get.currents = [16]*3 + data.data.cp_data[f"cp{i}"].data.set.charging_ev_data = Ev(i) + data.data.cp_data[f"cp{i}"].data.config.ev = i + data.data.counter_all_data.data.get.loadmanagement_prios = [ + {"type": "vehicle", "id": 3}, {"type": "vehicle", "id": 4}, {"type": "vehicle", "id": 5}] + data.data.ev_data["ev3"] = Ev(3) + data.data.ev_data["ev4"] = Ev(4) + data.data.ev_data["ev5"] = Ev(5) @dataclass @@ -111,6 +119,7 @@ class ParamsLimit(ParamsExpectedSetCurrent, ParamsExpectedCounterSet): expected_raw_power_left=0, expected_raw_currents_left_counter0=[21.53846153846154, 25.23076923076923, 25.23076923076923], expected_raw_currents_left_counter6=[16, 9.23076923076923, 9.23076923076923]), + # limit by unbalanced load ] @@ -137,19 +146,19 @@ def test_instant_charging_limit(params: ParamsLimit, all_cp_instant_charging_1p, @dataclass class ParamsControlParameter(ParamsExpectedSetCurrent, ParamsExpectedCounterSet): name: str = "" - prio_cp3: bool = False + full_power_cp3: bool = False submode_cp3: Chargemode = Chargemode.INSTANT_CHARGING - prio_cp4: bool = False + full_power_cp4: bool = False submode_cp4: Chargemode = Chargemode.INSTANT_CHARGING - prio_cp5: bool = False + full_power_cp5: bool = False submode_cp5: Chargemode = Chargemode.INSTANT_CHARGING cases_control_parameter = [ ParamsControlParameter(name="lift prio cp3", - prio_cp3=True, - prio_cp4=False, - prio_cp5=False, + full_power_cp3=True, + full_power_cp4=False, + full_power_cp5=False, expected_current_cp3=16, expected_current_cp4=8, expected_current_cp5=8, @@ -157,44 +166,38 @@ class ParamsControlParameter(ParamsExpectedSetCurrent, ParamsExpectedCounterSet) expected_raw_currents_left_counter0=[0]*3, expected_raw_currents_left_counter6=[0]*3), ParamsControlParameter(name="drop prio cp4", - prio_cp3=True, - prio_cp4=False, - prio_cp5=True, - expected_current_cp3=15, + full_power_cp3=True, + full_power_cp4=False, + full_power_cp5=True, + expected_current_cp3=16, expected_current_cp4=6, expected_current_cp5=10, - expected_raw_power_left=690, - expected_raw_currents_left_counter0=[1]*3, + expected_raw_power_left=0, + expected_raw_currents_left_counter0=[0]*3, expected_raw_currents_left_counter6=[0]*3), - ParamsControlParameter(name="lift submode cp4", + ParamsControlParameter(name="change submode cp4", submode_cp4=Chargemode.TIME_CHARGING, - expected_current_cp3=13, - expected_current_cp4=10, - expected_current_cp5=6, - expected_raw_power_left=2070, - expected_raw_currents_left_counter0=[3]*3, + expected_current_cp3=13.333333333333334, + expected_current_cp4=8, + expected_current_cp5=8, + expected_raw_power_left=1840, + expected_raw_currents_left_counter0=[2.666666666666666]*3, expected_raw_currents_left_counter6=[0]*3), - # ParamsControlParameter(name="drop submode cp4", - # niedrigster instant modus erreicht - # submode_cp4=Chargemode.PV_CHARGING, - # expected_current_cp3=16, - # expected_current_cp4=6, - # expected_current_cp5=10, - # expected_raw_power_left=0, - # expected_raw_currents_left_counter0=[0]*3, - # expected_raw_currents_left_counter6=[0]*3) ] @pytest.mark.parametrize("params", cases_control_parameter, ids=[c.name for c in cases_control_parameter]) def test_control_parameter_instant_charging(params: ParamsControlParameter, all_cp_instant_charging_3p, monkeypatch): # setup - data.data.cp_data["cp3"].data.control_parameter.prio = params.prio_cp3 data.data.cp_data["cp3"].data.control_parameter.submode = params.submode_cp3 - data.data.cp_data["cp4"].data.control_parameter.prio = params.prio_cp4 + data.data.cp_data["cp3"].data.set.charging_ev_data.data.full_power = params.full_power_cp3 + data.data.ev_data["ev3"].data.full_power = params.full_power_cp3 data.data.cp_data["cp4"].data.control_parameter.submode = params.submode_cp4 - data.data.cp_data["cp5"].data.control_parameter.prio = params.prio_cp5 + data.data.cp_data["cp4"].data.set.charging_ev_data.data.full_power = params.full_power_cp4 + data.data.ev_data["ev4"].data.full_power = params.full_power_cp4 data.data.cp_data["cp5"].data.control_parameter.submode = params.submode_cp5 + data.data.cp_data["cp5"].data.set.charging_ev_data.data.full_power = params.full_power_cp5 + data.data.ev_data["ev5"].data.full_power = params.full_power_cp5 data.data.counter_data["counter0"].data.set.raw_power_left = 22080 data.data.counter_data["counter0"].data.set.raw_currents_left = [32]*3 data.data.counter_data["counter6"].data.set.raw_currents_left = [16]*3 diff --git a/packages/control/algorithm/integration_test/pv_charging_test.py b/packages/control/algorithm/integration_test/pv_charging_test.py index 105e421470..d5688316f5 100644 --- a/packages/control/algorithm/integration_test/pv_charging_test.py +++ b/packages/control/algorithm/integration_test/pv_charging_test.py @@ -183,7 +183,7 @@ def test_pv_delay_expired(all_cp_pv_charging_3p, all_cp_not_charging, monkeypatc expected_current_cp3=16, expected_current_cp4=8, expected_current_cp5=8, - expected_raw_power_left=34820, + expected_raw_power_left=27920, expected_surplus_power_left=1090, expected_reserved_surplus=0, expected_released_surplus=0), @@ -195,7 +195,7 @@ def test_pv_delay_expired(all_cp_pv_charging_3p, all_cp_not_charging, monkeypatc expected_current_cp3=16, expected_current_cp4=7.8731884057971016, expected_current_cp5=7.8731884057971016, - expected_raw_power_left=24470, + expected_raw_power_left=17745, expected_surplus_power_left=1090, expected_reserved_surplus=0, expected_released_surplus=0), @@ -260,7 +260,7 @@ def test_surplus(params: ParamsSurplus, all_cp_pv_charging_3p, all_cp_charging_3 expected_current_cp3=32, expected_current_cp4=6, expected_current_cp5=6, - expected_raw_power_left=37520.0, + expected_raw_power_left=32460.0, expected_surplus_power_left=3000, expected_reserved_surplus=0, expected_released_surplus=0) diff --git a/packages/control/algorithm/min_current.py b/packages/control/algorithm/min_current.py index 8dada55ce0..fb00789cf7 100644 --- a/packages/control/algorithm/min_current.py +++ b/packages/control/algorithm/min_current.py @@ -5,7 +5,7 @@ from control.algorithm.chargemodes import CONSIDERED_CHARGE_MODES_MIN_CURRENT, CONSIDERED_CHARGE_MODES_PV_ONLY from control.chargepoint.chargepoint_state import ChargepointState from control.loadmanagement import Loadmanagement -from control.algorithm.filter_chargepoints import get_chargepoints_by_mode_and_counter +from control.algorithm.filter_chargepoints import get_chargepoints_by_mode_and_counter_and_lm_prio log = logging.getLogger(__name__) @@ -15,11 +15,16 @@ class MinCurrent: def __init__(self) -> None: pass - def set_min_current(self) -> None: - for mode_tuple, counter in common.mode_and_counter_generator(CONSIDERED_CHARGE_MODES_MIN_CURRENT): - preferenced_chargepoints = get_chargepoints_by_mode_and_counter(mode_tuple, f"counter{counter.num}") + def set_min_current(self, cp_prio_group) -> None: + log.info("**Mindestrom setzen**") + common.reset_current_to_target_current(cp_prio_group) + for counter in common.counter_generator(): + preferenced_chargepoints = get_chargepoints_by_mode_and_counter_and_lm_prio( + CONSIDERED_CHARGE_MODES_MIN_CURRENT, + f"counter{counter.num}", + cp_prio_group) if preferenced_chargepoints: - log.info(f"Mode-Tuple {mode_tuple[0]} - {mode_tuple[1]} - {mode_tuple[2]}, Zähler {counter.num}") + log.info(f"Zähler {counter.num}, Verbraucher {[f'LP{cp.num}' for cp in preferenced_chargepoints]}") common.update_raw_data(preferenced_chargepoints, diff_to_zero=True) while len(preferenced_chargepoints): cp = preferenced_chargepoints[0] @@ -43,7 +48,8 @@ def set_min_current(self) -> None: cp.data.control_parameter.min_current, cp) else: - if mode_tuple in CONSIDERED_CHARGE_MODES_PV_ONLY: + if (cp.data.control_parameter.chargemode, + cp.data.control_parameter.submode) in CONSIDERED_CHARGE_MODES_PV_ONLY: try: if (cp.data.control_parameter.state == ChargepointState.NO_CHARGING_ALLOWED or cp.data.control_parameter.state == ChargepointState.SWITCH_ON_DELAY): diff --git a/packages/control/algorithm/no_current.py b/packages/control/algorithm/no_current.py index 0b55fed852..7961f755bb 100644 --- a/packages/control/algorithm/no_current.py +++ b/packages/control/algorithm/no_current.py @@ -2,7 +2,7 @@ from control import data from control.algorithm.chargemodes import CONSIDERED_CHARGE_MODES_NO_CURRENT -from control.algorithm.filter_chargepoints import get_chargepoints_by_chargemodes +from control.algorithm.filter_chargepoints import get_chargepoints_by_mode log = logging.getLogger(__name__) @@ -12,7 +12,7 @@ def __init__(self) -> None: pass def set_no_current(self) -> None: - chargepoints = get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_NO_CURRENT) + chargepoints = get_chargepoints_by_mode(CONSIDERED_CHARGE_MODES_NO_CURRENT) for cp in chargepoints: cp.data.set.current = 0 diff --git a/packages/control/algorithm/surplus_controlled.py b/packages/control/algorithm/surplus_controlled.py index e2e894df51..5f9d38b767 100644 --- a/packages/control/algorithm/surplus_controlled.py +++ b/packages/control/algorithm/surplus_controlled.py @@ -1,12 +1,13 @@ import logging from typing import List, Optional, Tuple +from control.counter import limit_raw_power_left_to_surplus from control import data from control.algorithm import common from control.algorithm.chargemodes import (CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE, CONSIDERED_CHARGE_MODES_PV_ONLY, CONSIDERED_CHARGE_MODES_SURPLUS) -from control.algorithm.filter_chargepoints import (get_chargepoints_by_chargemodes, - get_chargepoints_by_mode_and_counter, +from control.algorithm.filter_chargepoints import (get_chargepoints_by_mode_and_counter_and_lm_prio, + get_chargepoints_by_mode_and_lm_prio, get_preferenced_chargepoint_charging) from control.algorithm.utils import get_medium_charging_current from control.chargepoint.charging_type import ChargingType @@ -26,30 +27,38 @@ class SurplusControlled: def __init__(self) -> None: pass - def set_surplus_current(self) -> None: - common.reset_current_by_chargemode(CONSIDERED_CHARGE_MODES_SURPLUS) - for mode_tuple, counter in common.mode_and_counter_generator(CONSIDERED_CHARGE_MODES_SURPLUS): + def set_surplus_current(self, cp_prio_group: List[Chargepoint]) -> None: + self.set_required_current_to_max(cp_prio_group) + evu_counter = data.data.counter_all_data.get_evu_counter() + limit_raw_power_left_to_surplus(evu_counter.calc_raw_surplus()) + if evu_counter.data.set.surplus_power_left < 0: + log.info("Keine Leistung für PV-geführtes Laden übrig.") + return + log.info("**PV-geführten Strom setzen**") + common.reset_current_to_target_current(cp_prio_group) + common.reset_current_by_chargemode(CONSIDERED_CHARGE_MODES_SURPLUS, cp_prio_group) + for counter in common.counter_generator(): preferenced_chargepoints, preferenced_cps_without_set_current = get_preferenced_chargepoint_charging( - get_chargepoints_by_mode_and_counter(mode_tuple, f"counter{counter.num}")) + get_chargepoints_by_mode_and_counter_and_lm_prio(CONSIDERED_CHARGE_MODES_SURPLUS, + f"counter{counter.num}", cp_prio_group)) cp_with_feed_in, cp_without_feed_in = self.filter_by_feed_in_limit(preferenced_chargepoints) if cp_without_feed_in: - self._set(cp_without_feed_in, 0, mode_tuple, counter) + self._set(cp_without_feed_in, 0, counter) feed_in_yield = data.data.general_data.data.chargemode_config.pv_charging.feed_in_yield if cp_with_feed_in: - self._set(cp_with_feed_in, feed_in_yield, mode_tuple, counter) + self._set(cp_with_feed_in, feed_in_yield, counter) if preferenced_cps_without_set_current: for cp in preferenced_cps_without_set_current: cp.data.set.current = cp.data.set.target_current - for cp in get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_SURPLUS): + for cp in get_chargepoints_by_mode_and_lm_prio(CONSIDERED_CHARGE_MODES_SURPLUS, cp_prio_group): if cp.data.control_parameter.state in CHARGING_STATES: self._fix_deviating_evse_current(cp) def _set(self, chargepoints: List[Chargepoint], feed_in_yield: Optional[int], - mode_tuple: Tuple[Optional[str], str, bool], counter: Counter) -> None: - log.info(f"Mode-Tuple {mode_tuple[0]} - {mode_tuple[1]} - {mode_tuple[2]}, Zähler {counter.num}") + log.info(f"Zähler {counter.num}, Verbraucher {[f'LP{cp.num}' for cp in chargepoints]}") common.update_raw_data(chargepoints, surplus=True) while len(chargepoints): cp = chargepoints[0] @@ -124,10 +133,10 @@ def _fix_deviating_evse_current(self, chargepoint: Chargepoint) -> float: log.debug(f"Ungenutzten Soll-Strom aufschlagen ergibt {current}A.") chargepoint.data.set.current = current - def check_submode_pv_charging(self) -> None: + def check_submode_pv_charging(self, cp_prio_group: List[Chargepoint]) -> None: evu_counter = data.data.counter_all_data.get_evu_counter() - for cp in get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_PV_ONLY): + for cp in get_chargepoints_by_mode_and_lm_prio(CONSIDERED_CHARGE_MODES_PV_ONLY, cp_prio_group): try: def phase_switch_necessary() -> bool: return (cp.cp_state_hw_support_phase_switch() and @@ -158,9 +167,9 @@ def phase_switch_necessary() -> bool: except Exception: log.exception(f"Fehler in der PV-gesteuerten Ladung bei {cp.num}") - def set_required_current_to_max(self) -> None: - for cp in get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_SURPLUS + - CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE): + def set_required_current_to_max(self, cp_prio_group: List[Chargepoint]) -> None: + for cp in get_chargepoints_by_mode_and_lm_prio(CONSIDERED_CHARGE_MODES_SURPLUS + + CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE, cp_prio_group): try: charging_ev_data = cp.data.set.charging_ev_data required_currents = cp.data.control_parameter.required_currents diff --git a/packages/control/algorithm/surplus_controlled_test.py b/packages/control/algorithm/surplus_controlled_test.py index 962eca835b..560ac6b478 100644 --- a/packages/control/algorithm/surplus_controlled_test.py +++ b/packages/control/algorithm/surplus_controlled_test.py @@ -4,7 +4,7 @@ from control import data from control.algorithm import surplus_controlled -from control.algorithm.filter_chargepoints import get_chargepoints_by_chargemodes +from control.algorithm.filter_chargepoints import get_chargepoints_by_mode from control.algorithm.surplus_controlled import (CONSIDERED_CHARGE_MODES_PV_ONLY, SurplusControlled, limit_adjust_current) from control.chargemode import Chargemode @@ -12,6 +12,7 @@ from control.chargepoint.chargepoint_data import Get, Set from control.chargepoint.chargepoint_template import CpTemplate from control.chargepoint.control_parameter import ControlParameter +from control.counter_all import CounterAll from control.ev.ev import Ev @@ -30,6 +31,12 @@ def mock_cp3() -> Chargepoint: return Chargepoint(3, None) +@pytest.fixture(autouse=True) +def mock_data() -> None: + data.data_init(Mock()) + data.data.counter_all_data = CounterAll() + + @pytest.mark.parametrize("feed_in_limit_1, feed_in_limit_2, feed_in_limit_3, expected_sorted", [pytest.param(True, True, True, ([mock_cp1, mock_cp2, mock_cp3], [])), pytest.param(True, False, True, ([mock_cp1, mock_cp3], [mock_cp2])), @@ -89,11 +96,11 @@ def test_set_required_current_to_max(phases: int, required_currents=required_currents)) mock_cp1.template = CpTemplate() mock_get_chargepoints_surplus_controlled = Mock(return_value=[mock_cp1]) - monkeypatch.setattr(surplus_controlled, "get_chargepoints_by_chargemodes", + monkeypatch.setattr(surplus_controlled, "get_chargepoints_by_mode_and_lm_prio", mock_get_chargepoints_surplus_controlled) # execution - SurplusControlled().set_required_current_to_max() + SurplusControlled().set_required_current_to_max([mock_cp1]) # evaluation assert mock_cp1.data.control_parameter.required_currents == expected_currents @@ -129,27 +136,34 @@ def test_add_unused_evse_current(evse_current: float, @pytest.mark.parametrize( - "submode_1, submode_2, expected_chargepoints", + "submode_1, submode_2, expected_cp_indices", [ - pytest.param(Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, [mock_cp1, mock_cp2]), - pytest.param(Chargemode.INSTANT_CHARGING, Chargemode.PV_CHARGING, [mock_cp2]), + pytest.param(Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, [1, 2]), + pytest.param(Chargemode.INSTANT_CHARGING, Chargemode.PV_CHARGING, [2]), pytest.param(Chargemode.INSTANT_CHARGING, Chargemode.INSTANT_CHARGING, []), ]) def test_get_chargepoints_submode_pv_charging(submode_1: Chargemode, submode_2: Chargemode, - expected_chargepoints: List[Chargepoint]): + expected_cp_indices: List[int], + mock_cp1, mock_cp2, mock_data): # setup def setup_cp(cp: Chargepoint, submode: str) -> Chargepoint: - cp.data.set.charging_ev_data = Ev(0) + cp.data.set.charging_ev_data = Ev(cp.num) + cp.data.config.ev = cp.num cp.data.control_parameter.chargemode = Chargemode.PV_CHARGING cp.data.control_parameter.submode = submode cp.data.control_parameter.required_current = 6 + cp.data.set.plug_time = 1 return cp data.data.cp_data = {"cp1": setup_cp(mock_cp1, submode_1), "cp2": setup_cp(mock_cp2, submode_2)} + data.data.counter_all_data.data.get.loadmanagement_prios = [ + {"type": "vehicle", "id": 1}, {"type": "vehicle", "id": 2}] # evaluation - chargepoints = get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_PV_ONLY) + chargepoints = get_chargepoints_by_mode(CONSIDERED_CHARGE_MODES_PV_ONLY) # assertion + cp_mapping = {1: mock_cp1, 2: mock_cp2} + expected_chargepoints = [cp_mapping[i] for i in expected_cp_indices] assert chargepoints == expected_chargepoints diff --git a/packages/control/bat_all.py b/packages/control/bat_all.py index 676ca7fd03..2df5cd023c 100644 --- a/packages/control/bat_all.py +++ b/packages/control/bat_all.py @@ -24,7 +24,7 @@ from control import data from control.algorithm.chargemodes import CONSIDERED_CHARGE_MODES_CHARGING -from control.algorithm.filter_chargepoints import get_chargepoints_by_chargemodes +from control.algorithm.filter_chargepoints import get_chargepoints_by_mode from control.pv import Pv from helpermodules.constants import NO_ERROR from modules.common.abstract_device import AbstractDevice @@ -316,7 +316,7 @@ def get_power_limit(self): if self.data.config.bat_control_permitted is False: self.data.set.power_limit = None else: - chargepoint_by_chargemodes = get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_CHARGING) + chargepoint_by_chargemodes = get_chargepoints_by_mode(CONSIDERED_CHARGE_MODES_CHARGING) # Falls aktive Steuerung an und Fahrzeuge laden und kein Überschuss im System ist, # dann Speicherleistung begrenzen. if (self.data.config.power_limit_mode != BatPowerLimitMode.NO_LIMIT.value and diff --git a/packages/control/bat_all_test.py b/packages/control/bat_all_test.py index 1429cdd1ad..a0c121bb46 100644 --- a/packages/control/bat_all_test.py +++ b/packages/control/bat_all_test.py @@ -209,8 +209,8 @@ def test_get_power_limit(params: PowerLimitParams, data_, monkeypatch): data.data.counter_data["counter0"].data.get.power = params.evu_power data.data.bat_all_data = b_all - get_chargepoints_by_chargemodes_mock = Mock(return_value=params.cps) - monkeypatch.setattr(bat_all, "get_chargepoints_by_chargemodes", get_chargepoints_by_chargemodes_mock) + get_chargepoints_by_mode_mock = Mock(return_value=params.cps) + monkeypatch.setattr(bat_all, "get_chargepoints_by_mode", get_chargepoints_by_mode_mock) get_evu_counter_mock = Mock(return_value=data.data.counter_data["counter0"]) monkeypatch.setattr(data.data.counter_all_data, "get_evu_counter", get_evu_counter_mock) get_controllable_bat_components_mock = Mock(return_value=[MqttBat(MqttBatSetup(id=2), device_id=0)]) diff --git a/packages/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index 328cfe899b..027788430b 100644 --- a/packages/control/chargepoint/chargepoint.py +++ b/packages/control/chargepoint/chargepoint.py @@ -245,7 +245,6 @@ def set_control_parameter(self, submode: str): else: self.data.control_parameter.chargemode = Chargemode( self.data.set.charge_template.data.chargemode.selected) - self.data.control_parameter.prio = self.data.set.charge_template.data.prio if self.template.data.charging_type == ChargingType.AC.value: self.data.control_parameter.min_current = self.data.set.charging_ev_data.ev_template.data.min_current else: @@ -710,7 +709,6 @@ 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", Priorität: {self.data.control_parameter.prio}" f", mittlerer Ist-Strom: {get_medium_charging_current(self.data.get.currents)}") except Exception: log.exception("Fehler im Prepare-Modul für Ladepunkt "+str(self.num)) @@ -828,7 +826,6 @@ def _pub_connected_vehicle(self, vehicle: Ev): charge_template=self.data.set.charge_template.data.id, ev_template=vehicle.ev_template.data.id, chargemode=self.data.set.charge_template.data.chargemode.selected, - priority=self.data.set.charge_template.data.prio, current_plan=current_plan, average_consumption=vehicle.ev_template.data.average_consump, time_charging_in_use=True if (self.data.control_parameter.submode == diff --git a/packages/control/chargepoint/control_parameter.py b/packages/control/chargepoint/control_parameter.py index ee671f705b..112ba4d043 100644 --- a/packages/control/chargepoint/control_parameter.py +++ b/packages/control/chargepoint/control_parameter.py @@ -17,7 +17,6 @@ class ControlParameter: "topic": "control_parameter/limit"}) min_current: int = field(default=6, metadata={"topic": "control_parameter/min_current"}) phases: int = field(default=0, metadata={"topic": "control_parameter/phases"}) - prio: bool = field(default=False, metadata={"topic": "control_parameter/prio"}) required_current: float = field(default=0, metadata={"topic": "control_parameter/required_current"}) required_currents: List[float] = field(default_factory=currents_list_factory) state: ChargepointState = field(default=ChargepointState.NO_CHARGING_ALLOWED, diff --git a/packages/control/counter.py b/packages/control/counter.py index b3e6fcddf0..94a9c4625d 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -8,7 +8,7 @@ from control import data from control.algorithm.chargemodes import CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE -from control.algorithm.filter_chargepoints import get_chargepoints_by_chargemodes +from control.algorithm.filter_chargepoints import get_chargepoints_by_mode from control.algorithm.utils import get_medium_charging_current from control.chargemode import Chargemode from control.chargepoint.chargepoint import Chargepoint @@ -194,21 +194,21 @@ def _set_power_left(self, loadmanagement_available: bool) -> None: else: self.data.set.raw_power_left = None - def update_values_left(self, diffs, cp_voltage: float) -> None: + def update_values_left(self, diffs: List[float], 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: 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') + log.debug(f'Zähler {self.num}: {self.data.set.raw_power_left}W verbleibende 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)) + def update_surplus_values_left(self, diffs: List[float], cp_voltage: float) -> None: if self.data.set.surplus_power_left: 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') + log.debug(f'Zähler {self.num}: {self.data.set.surplus_power_left}W verbleibender Überschuss') + + def update_currents_left(self, diffs: List[float]) -> None: + self.data.set.raw_currents_left = list(map(operator.sub, self.data.set.raw_currents_left, diffs)) + log.debug(f'Zähler {self.num}: {self.data.set.raw_currents_left}A verbleibende Ströme') def calc_surplus(self): # reservierte Leistung wird nicht berücksichtigt, weil diese noch verwendet werden kann, bis die EV @@ -519,7 +519,7 @@ def set_raw_surplus_power_left() -> None: """ grid_counter = data.data.counter_all_data.get_evu_counter() bidi_power = 0 - chargepoint_by_chargemodes = get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE) + chargepoint_by_chargemodes = get_chargepoints_by_mode(CONSIDERED_CHARGE_MODES_BIDI_DISCHARGE) for cp in chargepoint_by_chargemodes: bidi_power += cp.data.get.power grid_counter.data.set.surplus_power_left = grid_counter.data.get.power * -1 + bidi_power diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index 2eeb156d18..2fb0d8586d 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -4,9 +4,10 @@ from dataclasses import dataclass, field import logging import re -from typing import Callable, Dict, List, Optional, Tuple, Union +from typing import Callable, Dict, Generator, List, Optional, Tuple, Union from control import data +from control.chargepoint.chargepoint import Chargepoint from control.counter import Counter from dataclass_utils.factories import empty_list_factory from helpermodules.messaging import MessageType, pub_system_message @@ -52,6 +53,8 @@ class Set: class Get: hierarchy: List = field(default_factory=empty_list_factory, metadata={ "topic": "get/hierarchy"}) + loadmanagement_prios: List[Dict] = field( + default_factory=empty_list_factory, metadata={"topic": "get/loadmanagement_prios"}) def get_factory() -> Get: @@ -225,7 +228,7 @@ def get_chargepoints_of_counter(self, counter: str) -> List[str]: except KeyError: # Kein Ladepunkt unter dem Zähler pass - return self.connected_chargepoints + return list(reversed(self.connected_chargepoints)) def _get_all_cp_connected_to_counter(self, child: Dict) -> None: """ Rekursive Funktion, die alle Ladepunkte ermittelt, die an den angegebenen Zähler angeschlossen sind. @@ -481,6 +484,84 @@ def check_and_add(type_name: ComponentType, data_structure): "Lastmanagements gesetzt werden kann. Bitte zuerst einen EVU-Zähler hinzufügen."), MessageType.ERROR) + # Lastmanagement-Prioritäten + def loadmanagement_prios_add_item(self, new_id: int, new_type: ComponentType) -> None: + if new_type == ComponentType.VEHICLE: + self.data.get.loadmanagement_prios.append({"id": new_id, "type": new_type.value}) + else: + raise ValueError("Derzeit können nur Fahrzeuge zu den Lastmanagement-Prioritäten hinzugefügt werden.") + + def loadmanagement_prios_remove_item(self, id: int) -> None: + for item in self.data.get.loadmanagement_prios: + if item["id"] == id: + self.data.get.loadmanagement_prios.remove(item) + return + else: + raise IndexError(f"Element {id} konnte nicht in den Lastmanagement-Prioritäten gefunden werden.") + + def _get_prio_groups(self) -> List[Dict]: + groups = [] + current_group = [] + + for item in self.data.get.loadmanagement_prios: + if data.data.ev_data[f'ev{item["id"]}'].data.full_power: + if current_group: + groups.append({"items": current_group.copy(), "is_full_power_group": False}) + current_group.clear() + groups.append({"items": [item], "is_full_power_group": True}) + else: + current_group.append(item) + + if current_group: + groups.append({"items": current_group.copy(), "is_full_power_group": False}) + return groups + + def _convert_to_cp_group(self, group: Optional[Dict]) -> Optional[List[Counter]]: + if group is None: + return None + cp_group = [] + for item in group["items"]: + for cp in data.data.cp_data.values(): + if cp.data.config.ev == item["id"]: + cp_group.append(cp) + return cp_group + + def prio_groups_generator(self) -> Generator[Tuple[Optional[List[Chargepoint]], Optional[List[Chargepoint]]], + None, + None]: + groups = self._get_prio_groups() + start_index = 0 + skip_next = False + + if not groups: + yield None, None + + if groups[0]["is_full_power_group"]: + # Beginnt mit Blitz + next_full_power_group = groups[0] + next_low_power_group = None + start_index = 1 + yield None, self._convert_to_cp_group(next_full_power_group) + + for index in range(start_index, len(groups)): + try: + if skip_next: + skip_next = False + continue + if groups[index]["is_full_power_group"]: + next_full_power_group = groups[index] + next_low_power_group = None + else: + next_low_power_group = groups[index] + if index + 1 < len(groups) and groups[index + 1]["is_full_power_group"]: + next_full_power_group = groups[index + 1] + skip_next = True + else: + next_full_power_group = None + yield self._convert_to_cp_group(next_low_power_group), self._convert_to_cp_group(next_full_power_group) + except Exception: + log.exception("Fehler in der allgemeinen Zähler-Klasse") + def get_max_id_in_hierarchy(current_entry: List, max_id: int) -> int: for item in current_entry: diff --git a/packages/control/hierarchy_test.py b/packages/control/counter_all_hierarchy_test.py similarity index 99% rename from packages/control/hierarchy_test.py rename to packages/control/counter_all_hierarchy_test.py index 297a9d7a3b..a135709452 100644 --- a/packages/control/hierarchy_test.py +++ b/packages/control/counter_all_hierarchy_test.py @@ -142,7 +142,7 @@ def test_delete_discard_children(params: ParamsItem): cases_get_chargepoints_of_counter = [ - ParamsItem("get_chargepoints_of_counter", hierarchy_cp(), "counter2", expected_return=["cp3", "cp5", "cp6"]), + ParamsItem("get_chargepoints_of_counter", hierarchy_cp(), "counter2", expected_return=["cp6", "cp5", "cp3"]), ParamsItem("get_chargepoints_of_counter", hierarchy_two_level(), "counter0", expected_return=["cp2"]) ] diff --git a/packages/control/counter_all_prio_test.py b/packages/control/counter_all_prio_test.py new file mode 100644 index 0000000000..565e879c53 --- /dev/null +++ b/packages/control/counter_all_prio_test.py @@ -0,0 +1,145 @@ +from dataclasses import dataclass +from unittest.mock import Mock + +import pytest +from control import data +from control.chargepoint.chargepoint import Chargepoint +from control.counter_all import CounterAll +from control.ev.ev import Ev + + +@pytest.fixture() +def mock_data() -> None: + data.data_init(Mock()) + + +@dataclass +class EvFullPowerState: + ev0_full_power: bool = False + ev1_full_power: bool = False + ev2_full_power: bool = False + ev3_full_power: bool = False + ev4_full_power: bool = False + + +PRIO_LIST_FLASH_END = [{"type": "vehicle", "id": 0}, + {"type": "vehicle", "id": 1}, + {"type": "vehicle", "id": 2}, + {"type": "vehicle", "id": 4}] +PRIO_LIST_FLASH_START = [{"type": "vehicle", "id": 0}, + {"type": "vehicle", "id": 3}, + {"type": "vehicle", "id": 2}] +PRIO_LIST_DOUBLE_FLASH_START = [{"type": "vehicle", "id": 0}, + {"type": "vehicle", "id": 1}, + {"type": "vehicle", "id": 3}, + {"type": "vehicle", "id": 2}] +PRIO_LIST_DOUBLE_FLASH_MIDDLE = [{"type": "vehicle", "id": 4}, + {"type": "vehicle", "id": 0}, + {"type": "vehicle", "id": 1}, + {"type": "vehicle", "id": 3}, + {"type": "vehicle", "id": 2}] +PRIO_LIST_DOUBLE_FLASH_END = [{"type": "vehicle", "id": 4}, + {"type": "vehicle", "id": 0}, + {"type": "vehicle", "id": 1}] +PRIO_LIST_NO_FLASH_END = [{"type": "vehicle", "id": 4}, + {"type": "vehicle", "id": 1}] +PRIO_LIST_NO_FLASH_MIDDLE = [{"type": "vehicle", "id": 2}, + {"type": "vehicle", "id": 4}, + {"type": "vehicle", "id": 1}] +GROUPED_PAYLOAD_FLASH_MIDDLE = [ + {'items': [{'type': 'vehicle', 'id': 0}, {'type': 'vehicle', 'id': 1}], 'is_full_power_group': False}, + {'items': [{'type': 'vehicle', 'id': 2}], 'is_full_power_group': True}, + {'items': [{'type': 'vehicle', 'id': 4}], 'is_full_power_group': False}] +GROUPED_PAYLOAD_FLASH_START = [ + {'items': [{'type': 'vehicle', 'id': 0}], 'is_full_power_group': True}, + {'items': [{'type': 'vehicle', 'id': 3}, {'type': 'vehicle', 'id': 2}], 'is_full_power_group': False}] +GROUPED_PAYLOAD_DOUBLE_FLASH_START = [ + {'items': [{'type': 'vehicle', 'id': 0}], 'is_full_power_group': True}, + {'items': [{'type': 'vehicle', 'id': 1}], 'is_full_power_group': True}, + {'items': [{'type': 'vehicle', 'id': 3}, {'type': 'vehicle', 'id': 2}], 'is_full_power_group': False}] +GROUPED_PAYLOAD_DOUBLE_FLASH_MIDDLE = [ + {'items': [{'type': 'vehicle', 'id': 4}], 'is_full_power_group': False}, + {'items': [{'type': 'vehicle', 'id': 0}], 'is_full_power_group': True}, + {'items': [{'type': 'vehicle', 'id': 1}], 'is_full_power_group': True}, + {'items': [{'type': 'vehicle', 'id': 3}, {'type': 'vehicle', 'id': 2}], 'is_full_power_group': False}] +GROUPED_PAYLOAD_DOUBLE_FLASH_END = [ + {'items': [{'type': 'vehicle', 'id': 4}], 'is_full_power_group': False}, + {'items': [{'type': 'vehicle', 'id': 0}], 'is_full_power_group': True}, + {'items': [{'type': 'vehicle', 'id': 1}], 'is_full_power_group': True}] +GROUPED_PAYLOAD_FLASH_END = [ + {'items': [{'type': 'vehicle', 'id': 4}], 'is_full_power_group': False}, + {'items': [{'type': 'vehicle', 'id': 1}], 'is_full_power_group': True}] +GROUPED_PAYLOAD_NO_FLASH_MIDDLE = [ + {'items': [{'type': 'vehicle', 'id': 2}], 'is_full_power_group': True}, + {'items': [{'type': 'vehicle', 'id': 4}], 'is_full_power_group': False}, + {'items': [{'type': 'vehicle', 'id': 1}], 'is_full_power_group': True}] + + +@pytest.mark.parametrize("loadmanagement_prios, ev_full_power_state, expected_grouped_payload", [ + pytest.param(PRIO_LIST_FLASH_END, EvFullPowerState(ev2_full_power=True), GROUPED_PAYLOAD_FLASH_MIDDLE,), + pytest.param(PRIO_LIST_FLASH_START, EvFullPowerState(ev0_full_power=True), GROUPED_PAYLOAD_FLASH_START,), + pytest.param(PRIO_LIST_DOUBLE_FLASH_START, EvFullPowerState(ev0_full_power=True, ev1_full_power=True), + GROUPED_PAYLOAD_DOUBLE_FLASH_START,), + pytest.param(PRIO_LIST_DOUBLE_FLASH_MIDDLE, EvFullPowerState(ev0_full_power=True, ev1_full_power=True), + GROUPED_PAYLOAD_DOUBLE_FLASH_MIDDLE,), + pytest.param(PRIO_LIST_DOUBLE_FLASH_END, EvFullPowerState(ev0_full_power=True, ev1_full_power=True), + GROUPED_PAYLOAD_DOUBLE_FLASH_END,), + pytest.param(PRIO_LIST_NO_FLASH_END, EvFullPowerState(ev1_full_power=True), GROUPED_PAYLOAD_FLASH_END,), + pytest.param(PRIO_LIST_NO_FLASH_MIDDLE, EvFullPowerState(ev2_full_power=True, ev1_full_power=True), + GROUPED_PAYLOAD_NO_FLASH_MIDDLE,), +] +) +def test_get_prio_groups(loadmanagement_prios, + ev_full_power_state: EvFullPowerState, + expected_grouped_payload, + mock_data): + # setup + c = CounterAll() + c.data.get.loadmanagement_prios = loadmanagement_prios + for i in range(5): + data.data.ev_data[f"ev{i}"] = Ev(i) + data.data.ev_data[f"ev{i}"].data.full_power = getattr(ev_full_power_state, f"ev{i}_full_power") + + # execution + prio_groups = c._get_prio_groups() + + # verification + assert prio_groups == expected_grouped_payload + + +@pytest.mark.parametrize("prio_groups, expected_groups", [ + pytest.param(GROUPED_PAYLOAD_FLASH_MIDDLE, [([0, 1], [2]), ([4], None)]), + pytest.param(GROUPED_PAYLOAD_FLASH_START, [(None, [0]), ([3, 2], None)]), + pytest.param(GROUPED_PAYLOAD_DOUBLE_FLASH_START, [(None, [0]), (None, [1]), ([3, 2], None)]), + pytest.param(GROUPED_PAYLOAD_DOUBLE_FLASH_MIDDLE, [([4], [0]), (None, [1]), ([3, 2], None)]), + pytest.param(GROUPED_PAYLOAD_DOUBLE_FLASH_END, [([4], [0]), (None, [1])]), + pytest.param(GROUPED_PAYLOAD_FLASH_END, [([4], [1])]), + pytest.param(GROUPED_PAYLOAD_NO_FLASH_MIDDLE, [(None, [2]), ([4], [1])]), +] +) +def test_prio_groups_generator(prio_groups, expected_groups, mock_data, monkeypatch): + # setup + c = CounterAll() + for i in range(5): + data.data.ev_data[f"ev{i}"] = Ev(i) + for i in range(5): + data.data.cp_data[f"cp{i}"] = Chargepoint(i, None) + data.data.cp_data[f"cp{i}"].data.config.ev = i + + mock_get_prio_groups = Mock(return_value=prio_groups) + monkeypatch.setattr(CounterAll, "_get_prio_groups", mock_get_prio_groups) + + # execution + gen = c.prio_groups_generator() + + # verification + for t in expected_groups: + generated = next(gen) + if t[0] is None: + assert generated[0] is None + else: + assert [cp.num for cp in generated[0]] == t[0] + if t[1] is None: + assert generated[1] is None + else: + assert [cp.num for cp in generated[1]] == t[1] diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 11c66be9d5..916deac981 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -110,7 +110,6 @@ def chargemode_factory() -> Chargemode: class ChargeTemplateData: id: int = 0 name: str = "Lade-Profil" - prio: bool = False load_default: bool = False time_charging: TimeCharging = field(default_factory=time_charging_factory) chargemode: Chargemode = field(default_factory=chargemode_factory) diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index ae2d1aa02b..25ecc812e7 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -39,7 +39,8 @@ def get_vehicle_default() -> dict: "model": None, }, "tag_id": [], - "get/soc": 0 + "get/soc": 0, + "full_power": False, } @@ -76,6 +77,7 @@ class EvData: set: Set = field(default_factory=set_factory) charge_template: int = field(default=0, metadata={"topic": "charge_template"}) ev_template: int = field(default=0, metadata={"topic": "ev_template"}) + full_power: bool = field(default=False, metadata={"topic": "full_power"}) name: str = field(default="neues Fahrzeug", metadata={"topic": "name"}) tag_id: List[str] = field(default_factory=empty_list_factory, metadata={ "topic": "tag_id"}) diff --git a/packages/helpermodules/command.py b/packages/helpermodules/command.py index 1770d4ba89..f7fc630cd8 100644 --- a/packages/helpermodules/command.py +++ b/packages/helpermodules/command.py @@ -286,10 +286,10 @@ def setup_added_chargepoint(): chargepoint_config["id"] = new_id chargepoint_config["name"] = f'{chargepoint_config["name"]} {new_id}' try: - evu_counter = data.data.counter_all_data.get_id_evu_counter() - data.data.counter_all_data.hierarchy_add_item_below( + evu_counter = SubData.counter_all_data.get_id_evu_counter() + SubData.counter_all_data.hierarchy_add_item_below( new_id, ComponentType.CHARGEPOINT, evu_counter) - Pub().pub("openWB/set/counter/get/hierarchy", data.data.counter_all_data.data.get.hierarchy) + Pub().pub("openWB/set/counter/get/hierarchy", SubData.counter_all_data.data.get.hierarchy) setup_added_chargepoint() except (TypeError, IndexError): if chargepoint_config["type"] == 'internal_openwb' and SubData.general_data.data.extern: @@ -299,9 +299,9 @@ def setup_added_chargepoint(): "type": ComponentType.CHARGEPOINT.value, "children": [] }] + - data.data.counter_all_data.data.get.hierarchy) + SubData.counter_all_data.data.get.hierarchy) Pub().pub("openWB/set/counter/get/hierarchy", hierarchy) - data.data.counter_all_data.data.get.hierarchy = hierarchy + SubData.counter_all_data.data.get.hierarchy = hierarchy setup_added_chargepoint() else: pub_user_message(payload, connection_id, @@ -350,8 +350,8 @@ def removeChargepoint(self, connection_id: str, payload: dict) -> None: f'Die ID \'{payload["data"]["id"]}\' ist größer als die maximal vergebene ' f'ID \'{self.max_id_hierarchy}\'.', MessageType.ERROR) ProcessBrokerBranch(f'chargepoint/{payload["data"]["id"]}/').remove_topics() - data.data.counter_all_data.hierarchy_remove_item(payload["data"]["id"]) - Pub().pub("openWB/set/counter/get/hierarchy", data.data.counter_all_data.data.get.hierarchy) + SubData.counter_all_data.hierarchy_remove_item(payload["data"]["id"]) + Pub().pub("openWB/set/counter/get/hierarchy", SubData.counter_all_data.data.get.hierarchy) pub_user_message(payload, connection_id, f'Ladepunkt mit ID \'{payload["data"]["id"]}\' gelöscht.', MessageType.SUCCESS) @@ -709,6 +709,9 @@ def addVehicle(self, connection_id: str, payload: dict) -> None: self.addChargeTemplate("addVehicle", {}) if self.max_id_ev_template == -1: self.addEvTemplate("addVehicle", {}) + SubData.counter_all_data.loadmanagement_prios_add_item(new_id, ComponentType.VEHICLE) + Pub().pub("openWB/set/counter/get/loadmanagement_prios", + SubData.counter_all_data.data.get.loadmanagement_prios) pub_user_message(payload, connection_id, f'Neues EV mit ID \'{new_id}\' hinzugefügt.', MessageType.SUCCESS) def removeVehicle(self, connection_id: str, payload: dict) -> None: @@ -720,6 +723,9 @@ def removeVehicle(self, connection_id: str, payload: dict) -> None: if payload["data"]["id"] > 0: Pub().pub(f'openWB/vehicle/{payload["data"]["id"]}', "") ProcessBrokerBranch(f'vehicle/{payload["data"]["id"]}/').remove_topics() + SubData.counter_all_data.loadmanagement_prios_remove_item(payload["data"]["id"]) + Pub().pub("openWB/set/counter/get/loadmanagement_prios", + SubData.counter_all_data.data.get.loadmanagement_prios) pub_user_message( payload, connection_id, f'EV mit ID \'{payload["data"]["id"]}\' gelöscht.', MessageType.SUCCESS) diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 0ded855015..e0682e9285 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -367,6 +367,8 @@ def process_vehicle_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, str) elif "/info" in msg.topic: self._validate_value(msg, "json") + elif "/full_power" in msg.topic: + self._validate_value(msg, bool) elif "openWB/set/vehicle/set/vehicle_update_completed" in msg.topic: self._validate_value(msg, bool) elif "/set/soc_error_counter" in msg.topic: @@ -913,7 +915,8 @@ def process_counter_topic(self, msg: mqtt.MQTTMessage): "openWB/set/counter/set/daily_yield_home_consumption" in msg.topic or "openWB/set/counter/set/disengageable_smarthome_power" in msg.topic): self._validate_value(msg, float, [(0, float("inf"))]) - elif "openWB/set/counter/get/hierarchy" in msg.topic: + elif ("openWB/set/counter/get/hierarchy" in msg.topic or + "openWB/set/counter/get/loadmanagement_prios" in msg.topic): self._validate_value(msg, None) elif "openWB/set/counter/config/home_consumption_source_id" in msg.topic: self._validate_value(msg, int) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 15530c627d..35d75d363f 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -11,6 +11,7 @@ from typing import List, Optional from paho.mqtt.client import Client as MqttClient, MQTTMessage +from control.chargemode import Chargemode from control.limiting_value import LoadmanagementLimit import dataclass_utils @@ -57,7 +58,7 @@ class UpdateConfig: - DATASTORE_VERSION = 107 + DATASTORE_VERSION = 108 valid_topic = [ "^openWB/bat/config/bat_control_permitted$", @@ -174,6 +175,7 @@ class UpdateConfig: "^openWB/counter/config/consider_less_charging$", "^openWB/counter/config/home_consumption_source_id$", "^openWB/counter/get/hierarchy$", + "^openWB/counter/get/loadmanagement_prios$", "^openWB/counter/set/disengageable_smarthome_power$", "^openWB/counter/set/imported_home_consumption$", "^openWB/counter/set/invalid_home_consumption$", @@ -359,6 +361,7 @@ class UpdateConfig: "^openWB/vehicle/[0-9]+/ev_template$", "^openWB/vehicle/[0-9]+/name$", "^openWB/vehicle/[0-9]+/info$", + "^openWB/vehicle/[0-9]+/full_power$", "^openWB/vehicle/[0-9]+/soc_module/calculated_soc_state$", "^openWB/vehicle/[0-9]+/soc_module/config$", "^openWB/vehicle/[0-9]+/soc_module/general_config$", @@ -519,6 +522,7 @@ class UpdateConfig: ("openWB/chargepoint/get/power", 0), ("openWB/chargepoint/template/0", get_chargepoint_template_default()), ("openWB/counter/get/hierarchy", []), + ("openWB/counter/get/loadmanagement_prios", [{"id": 0, "type": "vehicle"}]), ("openWB/counter/config/consider_less_charging", counter_all.Config().consider_less_charging), ("openWB/counter/config/home_consumption_source_id", counter_all.Config().home_consumption_source_id), ("openWB/vehicle/0/name", "Standard-Fahrzeug"), @@ -2701,3 +2705,33 @@ def upgrade(topic: str, payload) -> None: return {topic: provider} self._loop_all_received_topics(upgrade) self._append_datastore_version(107) + + def upgrade_datastore_108(self) -> None: + def upgrade(topic: str, payload) -> None: + if re.search("openWB/vehicle/[0-9]+/name$", topic) is not None: + return {topic.replace("/name", "/full_power"): False} + self._loop_all_received_topics(upgrade) + + CHARGEMODES = ((Chargemode.SCHEDULED_CHARGING.value, True), + (Chargemode.SCHEDULED_CHARGING.value, False), + (Chargemode.INSTANT_CHARGING.value, True), + (Chargemode.INSTANT_CHARGING.value, False), + (Chargemode.ECO_CHARGING.value, True), + (Chargemode.PV_CHARGING.value, True), + (Chargemode.PV_CHARGING.value, False), + (Chargemode.STOP.value, True), + (Chargemode.STOP.value, False),) + + def upgrade2(topic: str, payload) -> None: + if re.search("openWB/vehicle/[0-9]+/charge_template", topic) is not None: + charge_template_id = decode_payload(payload) + charge_template = decode_payload( + self.all_received_topics[f"openWB/vehicle/template/charge_template/{charge_template_id}"]) + if charge_template["chargemode"]["selected"] == chargemode and charge_template["prio"] == prio: + loadmanagement_prios.append({"type": "vehicle", "id": int(get_index(topic))}) + + loadmanagement_prios = [] + for chargemode, prio in CHARGEMODES: + self._loop_all_received_topics(upgrade2) + self.__update_topic("openWB/counter/get/loadmanagement_prios", loadmanagement_prios) + self._append_datastore_version(108) diff --git a/packages/modules/common/component_type.py b/packages/modules/common/component_type.py index a759d3c533..a04436c153 100644 --- a/packages/modules/common/component_type.py +++ b/packages/modules/common/component_type.py @@ -6,11 +6,13 @@ class ComponentType(Enum): BACKUP_CLOUD = "backup_cloud" BAT = "bat" CHARGEPOINT = "cp" + CONSUMER = "consumer" COUNTER = "counter" FLEXIBLE_TARIFF = "dynamic_tariff" GRID_FEE = "grid_tariff" INVERTER = "inverter" IO = "io" + VEHICLE = "vehicle" def special_to_general_type_mapping(component_type: str) -> ComponentType: