diff --git a/data/config/mosquitto/local/openwb_local.conf b/data/config/mosquitto/local/openwb_local.conf index f540b3dcf1..8d37590972 100644 --- a/data/config/mosquitto/local/openwb_local.conf +++ b/data/config/mosquitto/local/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/conftest.py b/packages/conftest.py index b58a69d591..fb6cfc8d44 100644 --- a/packages/conftest.py +++ b/packages/conftest.py @@ -12,7 +12,7 @@ from control.counter import Config as CounterConfig from control.counter import Get as CounterGet from control.counter import Set as CounterSet -from control.counter_all import CounterAll +from control.counter_all.counter_all import CounterAll from control.pv import Pv, PvData from control.pv import Config as PvConfig from control.pv import Get as PvGet diff --git a/packages/control/algorithm/additional_current.py b/packages/control/algorithm/additional_current.py index 76ee2d0031..87893fa10e 100644 --- a/packages/control/algorithm/additional_current.py +++ b/packages/control/algorithm/additional_current.py @@ -1,5 +1,6 @@ import logging +from control import data from control.algorithm import common from control.algorithm.chargemodes import CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT from control.limiting_value import LoadmanagementLimit @@ -24,9 +25,9 @@ def set_additional_current(self) -> None: 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}") - while len(preferenced_chargepoints): - cp = preferenced_chargepoints[0] - missing_currents, counts = common.get_missing_currents_left(preferenced_chargepoints) + for cp, group in data.data.counter_all_data.generator_cps_by_loadmanagement_prios( + preferenced_chargepoints): + missing_currents, counts = common.get_missing_currents_left(group) available_currents, limit = Loadmanagement().get_available_currents(missing_currents, counter, cp) log.debug(f"cp {cp.num} available currents {available_currents} missing currents " f"{missing_currents} limit {limit.message}") @@ -40,7 +41,6 @@ def set_additional_current(self) -> None: cp.data.control_parameter.min_current, current, cp) - preferenced_chargepoints.pop(0) if preferenced_cps_without_set_current: for cp in preferenced_cps_without_set_current: cp.data.set.current = cp.data.set.target_current diff --git a/packages/control/algorithm/bidi_charging.py b/packages/control/algorithm/bidi_charging.py index 4dec5a7b4c..91e4b090fd 100644 --- a/packages/control/algorithm/bidi_charging.py +++ b/packages/control/algorithm/bidi_charging.py @@ -20,9 +20,8 @@ def set_bidi(self): if preferenced_cps: log.info( f"Mode-Tuple {mode_tuple[0]} - {mode_tuple[1]} - {mode_tuple[2]}, Zähler {grid_counter.num}") - while len(preferenced_cps): - cp = preferenced_cps[0] - zero_point_adjustment = grid_counter.data.set.surplus_power_left / len(preferenced_cps) + for cp, group in data.data.counter_all_data.generator_cps_by_loadmanagement_prios(preferenced_cps): + zero_point_adjustment = grid_counter.data.set.surplus_power_left / len(group) 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)] @@ -43,4 +42,3 @@ def set_bidi(self): 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) diff --git a/packages/control/algorithm/common_test.py b/packages/control/algorithm/common_test.py index 707d88c0a2..df53dbb7c2 100644 --- a/packages/control/algorithm/common_test.py +++ b/packages/control/algorithm/common_test.py @@ -8,7 +8,7 @@ from control.chargepoint.chargepoint import Chargepoint from control.ev.ev import Ev from control.counter import Counter -from control.counter_all import CounterAll +from control.counter_all.counter_all import CounterAll from control.io_device import IoActions diff --git a/packages/control/algorithm/filter_chargepoints.py b/packages/control/algorithm/filter_chargepoints.py index f523b1d399..041bd337a6 100644 --- a/packages/control/algorithm/filter_chargepoints.py +++ b/packages/control/algorithm/filter_chargepoints.py @@ -47,7 +47,7 @@ def get_chargepoints_with_required_current_by_chargemode( def get_preferenced_chargepoint_charging( chargepoints: List[Chargepoint]) -> Tuple[List[Chargepoint], List[Chargepoint]]: - preferenced_chargepoints = _get_preferenced_chargepoint(chargepoints) + preferenced_chargepoints = get_preferenced_chargepoint(chargepoints) preferenced_chargepoints_with_set_current = [] preferenced_chargepoints_without_set_current = [] for cp in preferenced_chargepoints: @@ -67,7 +67,7 @@ def get_preferenced_chargepoint_charging( # tested -def _get_preferenced_chargepoint(valid_chargepoints: List[Chargepoint]) -> List: +def get_preferenced_chargepoint(valid_chargepoints: List[Chargepoint]) -> List: """ermittelt die Ladepunkte in der Reihenfolge, in der sie geladen/gestoppt werden sollen. Die Bedingungen sind: geringste Mindeststromstärke, niedrigster SoC, frühester Ansteck-Zeitpunkt(Einschalten)/Lademenge(Abschalten), diff --git a/packages/control/algorithm/filter_chargepoints_test.py b/packages/control/algorithm/filter_chargepoints_test.py index def2524811..68078b266b 100644 --- a/packages/control/algorithm/filter_chargepoints_test.py +++ b/packages/control/algorithm/filter_chargepoints_test.py @@ -10,7 +10,7 @@ from control.chargepoint.chargepoint import Chargepoint, ChargepointData from control.chargepoint.chargepoint_data import Log, Set from control.chargepoint.control_parameter import ControlParameter -from control.counter_all import CounterAll +from control.counter_all.counter_all import CounterAll from control.ev.ev import Ev, EvData, Get @@ -78,7 +78,7 @@ def mock_cp(cp: Chargepoint, num: int): cp2 = mock_cp(mock_cp2, 2) cp3 = mock_cp(mock_cp3, 3) # execution - preferenced_chargepoints = filter_chargepoints._get_preferenced_chargepoint([cp1, cp2, cp3]) + preferenced_chargepoints = filter_chargepoints.get_preferenced_chargepoint([cp1, cp2, cp3]) # evaluation assert preferenced_chargepoints == params.expected_sort diff --git a/packages/control/algorithm/integration_test/conftest.py b/packages/control/algorithm/integration_test/conftest.py index dd91361d37..bb02b7c782 100644 --- a/packages/control/algorithm/integration_test/conftest.py +++ b/packages/control/algorithm/integration_test/conftest.py @@ -8,7 +8,7 @@ from control.bat_all import BatAll from control.chargepoint.chargepoint import Chargepoint from control.chargepoint.chargepoint_template import CpTemplate -from control.counter_all import CounterAll +from control.counter_all.counter_all import CounterAll from control.counter import Counter from control.ev.ev import Ev from control.io_device import IoActions @@ -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 = i 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,13 @@ 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": "group", + "label": "Fahrzeuge", + "children": [ + {"type": "vehicle", "id": 3}, {"type": "vehicle", "id": 4}, {"type": "vehicle", "id": 5} + ] + }] data.data.counter_all_data.data.config.consider_less_charging = True data.data.io_actions = IoActions() diff --git a/packages/control/algorithm/min_current.py b/packages/control/algorithm/min_current.py index 48ce9eba6a..634404d7db 100644 --- a/packages/control/algorithm/min_current.py +++ b/packages/control/algorithm/min_current.py @@ -21,8 +21,8 @@ def set_min_current(self) -> None: if preferenced_chargepoints: log.info(f"Mode-Tuple {mode_tuple[0]} - {mode_tuple[1]} - {mode_tuple[2]}, Zähler {counter.num}") common.update_raw_data(preferenced_chargepoints, diff_to_zero=True) - while len(preferenced_chargepoints): - cp = preferenced_chargepoints[0] + for cp, group in data.data.counter_all_data.generator_cps_by_loadmanagement_prios( + preferenced_chargepoints): missing_currents, counts = common.get_min_current(cp) if max(missing_currents) > 0: available_currents, limit = Loadmanagement().get_available_currents( @@ -52,4 +52,3 @@ def set_min_current(self) -> None: except Exception: log.exception(f"Fehler in der PV-gesteuerten Ladung bei {cp.num}") cp.data.set.current = 0 - preferenced_chargepoints.pop(0) diff --git a/packages/control/algorithm/surplus_controlled.py b/packages/control/algorithm/surplus_controlled.py index d7239b9696..144a655b53 100644 --- a/packages/control/algorithm/surplus_controlled.py +++ b/packages/control/algorithm/surplus_controlled.py @@ -51,9 +51,8 @@ def _set(self, counter: Counter) -> None: log.info(f"Mode-Tuple {mode_tuple[0]} - {mode_tuple[1]} - {mode_tuple[2]}, Zähler {counter.num}") common.update_raw_data(chargepoints, surplus=True) - while len(chargepoints): - cp = chargepoints[0] - missing_currents, counts = common.get_missing_currents_left(chargepoints) + for cp, group in data.data.counter_all_data.generator_cps_by_loadmanagement_prios(chargepoints): + missing_currents, counts = common.get_missing_currents_left(group) available_currents, limit = Loadmanagement().get_available_currents_surplus( missing_currents, voltages_mean(cp.data.get.voltages), @@ -88,7 +87,6 @@ def _set(self, limited_current, cp, surplus=True) - chargepoints.pop(0) def _set_loadmangement_message(self, current: float, diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py deleted file mode 100644 index 2eeb156d18..0000000000 --- a/packages/control/counter_all.py +++ /dev/null @@ -1,497 +0,0 @@ -"""Zähler-Logik -""" -import copy -from dataclasses import dataclass, field -import logging -import re -from typing import Callable, Dict, List, Optional, Tuple, Union - -from control import data -from control.counter import Counter -from dataclass_utils.factories import empty_list_factory -from helpermodules.messaging import MessageType, pub_system_message -from helpermodules.pub import Pub -from modules.common.component_type import ComponentType, component_type_to_readable_text -from modules.common.fault_state import FaultStateLevel -from modules.common.simcount import SimCounter - -log = logging.getLogger(__name__) - - -@dataclass -class Config: - home_consumption_source_id: Optional[str] = field( - default=None, metadata={"topic": "config/home_consumption_source_id"}) - consider_less_charging: bool = field( - default=False, metadata={"topic": "config/consider_less_charging"}) - - -def config_factory() -> Config: - return Config() - - -@dataclass -class Set: - loadmanagement_active: bool = field( - default=False, metadata={"topic": "set/loadmanagement_active"}) - home_consumption: float = field(default=0, metadata={"topic": "set/home_consumption"}) - smarthome_power_excluded_from_home_consumption: float = field( - default=0, - metadata={"topic": "set/smarthome_power_excluded_from_home_consumption"}) - invalid_home_consumption: int = field( - default=0, metadata={"topic": "set/invalid_home_consumption"}) - daily_yield_home_consumption: float = field( - default=0, metadata={"topic": "set/daily_yield_home_consumption"}) - imported_home_consumption: float = field( - default=0, metadata={"topic": "set/imported_home_consumption"}) - disengageable_smarthome_power: float = field( - default=0, metadata={"topic": "set/disengageable_smarthome_power"}) - - -@dataclass -class Get: - hierarchy: List = field(default_factory=empty_list_factory, metadata={ - "topic": "get/hierarchy"}) - - -def get_factory() -> Get: - return Get() - - -def set_factory() -> Set: - return Set() - - -@dataclass -class CounterAllData: - config: Config = field(default_factory=config_factory) - get: Get = field(default_factory=get_factory) - set: Set = field(default_factory=set_factory) - - -class CounterAll: - MISSING_EVU_COUNTER = "Bitte erst einen EVU-Zähler konfigurieren." - - def __init__(self): - self.data = CounterAllData() - # Hilfsvariablen für die rekursiven Funktionen - self.connected_counters = [] - self.connected_chargepoints = [] - self.childless = [] - self.sim_counter = SimCounter("", "", prefix="bezug") - self.sim_counter.topic = "openWB/set/counter/set/" - - def get_evu_counter(self) -> Counter: - return data.data.counter_data[f"counter{self.get_id_evu_counter()}"] - - def get_evu_counter_str(self) -> str: - return f"counter{self.get_id_evu_counter()}" - - def get_id_evu_counter(self) -> int: - try: - if ComponentType.COUNTER.value == self.data.get.hierarchy[0]["type"]: - return self.data.get.hierarchy[0]['id'] - else: - raise TypeError - except Exception: - log.error( - "Ohne Konfiguration eines EVU-Zählers an der Spitze der Hierarchie ist keine Regelung und keine Ladung " - "möglich.") - raise - - def set_home_consumption(self) -> None: - try: - self._validate_home_consumption_counter() - home_consumption, elements = self._calc_home_consumption() - if home_consumption < 0: - log.error( - f"Ungültiger Hausverbrauch: {home_consumption}W, Berücksichtigte Komponenten neben EVU {elements}") - if self.data.config.home_consumption_source_id is None: - hc_counter_source = self.get_evu_counter_str() - else: - hc_counter_source = f"counter{self.data.config.home_consumption_source_id}" - hc_counter_data = data.data.counter_data[hc_counter_source].data - if hc_counter_data.get.fault_state == FaultStateLevel.NO_ERROR: - hc_counter_data.get.fault_state = FaultStateLevel.WARNING.value - hc_counter_data.get.fault_str = ("Hinweis: Es gibt mehr Stromerzeuger im Haus als in der openWB " - "eingetragen sind. Der Hausverbrauch kann nicht korrekt berechnet " - "werden. Dies hat auf die PV-Überschussladung keine negativen " - "Auswirkungen.") - if self.data.set.invalid_home_consumption < 3: - self.data.set.invalid_home_consumption += 1 - return - else: - home_consumption = 0 - else: - self.data.set.invalid_home_consumption = 0 - self.data.set.home_consumption = home_consumption - imported, _ = self.sim_counter.sim_count(self.data.set.home_consumption) - self.data.set.imported_home_consumption = imported - except Exception: - log.exception("Fehler in der allgemeinen Zähler-Klasse") - - EVU_IS_HC_COUNTER_ERROR = ("Der EVU-Zähler kann nicht als Quelle für den Hausverbrauch verwendet werden. Meist ist " - "der Zähler am EVU-Punkt installiert, dann muss im Lastmanagement unter Hausverbrauch" - " 'von openWB berechnen' ausgewählt werden. Wenn der Zähler im Hausverbrauchszweig " - "installiert ist, einen virtuellen Zähler anlegen und im Lastmanagement ganz links " - "anordnen.") - - def _validate_home_consumption_counter(self): - if self.data.config.home_consumption_source_id is not None: - if self.data.config.home_consumption_source_id == self.get_id_evu_counter(): - hc_counter_data = data.data.counter_data[self.get_evu_counter_str()].data - hc_counter_data.get.fault_state = FaultStateLevel.ERROR.value - hc_counter_data.get.fault_str = self.EVU_IS_HC_COUNTER_ERROR - evu_counter = self.get_id_evu_counter() - Pub().pub(f"openWB/set/counter/{evu_counter}/get/fault_state", - hc_counter_data.get.fault_state) - Pub().pub(f"openWB/set/counter/{evu_counter}/get/fault_str", - hc_counter_data.get.fault_str) - raise Exception(self.EVU_IS_HC_COUNTER_ERROR) - - def _calc_home_consumption(self) -> Tuple[float, List]: - power = 0 - if self.data.config.home_consumption_source_id is None: - id_source = self.get_id_evu_counter() - else: - id_source = self.data.config.home_consumption_source_id - elements_to_sum_up = self.get_elements_for_downstream_calculation(id_source) - for element in elements_to_sum_up: - if element["type"] == ComponentType.CHARGEPOINT.value: - component = data.data.cp_data[f"cp{element['id']}"] - elif element["type"] == ComponentType.BAT.value: - component = data.data.bat_data[f"bat{element['id']}"] - elif element["type"] == ComponentType.COUNTER.value: - component = data.data.counter_data[f"counter{element['id']}"] - elif element["type"] == ComponentType.INVERTER.value: - component = data.data.pv_data[f"pv{element['id']}"] - - if component.data.get.fault_state < 2: - power += component.data.get.power - else: - log.warning( - f"Komponente {element['type']}{component.num} ist im Fehlerzustand und wird nicht berücksichtigt.") - evu = data.data.counter_data[f"counter{id_source}"].data.get.power - return evu - power - self.data.set.smarthome_power_excluded_from_home_consumption, elements_to_sum_up - - def _add_hybrid_bat(self, id: int) -> List: - elements = [] - inverter_children = self.get_entry_of_element(id)["children"] - for child in inverter_children: - if child["type"] == ComponentType.BAT.value: - elements.append(child) - return elements - - def get_elements_for_downstream_calculation(self, id: int): - """returns a list of elements that are relevant for the calculation of the counter values based on the - downstream components, eg home consumption or virtual counter.""" - elements = copy.deepcopy(self.get_entry_of_element(id)["children"]) - elements_to_sum_up = elements - for element in elements: - if element["type"] == ComponentType.INVERTER.value: - elements_to_sum_up.extend(self._add_hybrid_bat(element['id'])) - return elements_to_sum_up - - # Hierarchie analysieren - - def get_all_elements_without_children(self, id: int) -> List[Dict]: - self.childless.clear() - self.get_all_elements_without_children_recursive(self.get_entry_of_element(id)) - return self.childless - - def get_all_elements_without_children_recursive(self, child: Dict) -> None: - for child in child["children"]: - try: - if len(child["children"]) != 0: - self.get_all_elements_without_children_recursive(child) - else: - self.childless.append(child) - except Exception: - log.exception("Fehler in der allgemeinen Zähler-Klasse") - - def get_chargepoints_of_counter(self, counter: str) -> List[str]: - """ gibt eine Liste der Ladepunkte, die in den folgenden Zweigen des Zählers sind, zurück. - """ - self.connected_chargepoints.clear() - if counter == self.get_evu_counter_str(): - counter_object = self.data.get.hierarchy[0] - else: - counter_object = self.__get_entry( - self.data.get.hierarchy[0], - int(counter[7:]), - self.__get_entry_of_element) - try: - self._get_all_cp_connected_to_counter(counter_object) - except KeyError: - # Kein Ladepunkt unter dem Zähler - pass - return 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. - """ - # Alle Objekte der Ebene durchgehen - for child in child["children"]: - try: - if child["type"] == ComponentType.CHARGEPOINT.value: - self.connected_chargepoints.append(f"cp{child['id']}") - # Wenn das Objekt noch Kinder hat, diese ebenfalls untersuchen. - elif len(child["children"]) != 0: - self._get_all_cp_connected_to_counter(child) - except Exception: - log.exception("Fehler in der allgemeinen Zähler-Klasse") - - def get_counters_to_check(self, num: int) -> List[str]: - """ ermittelt alle Zähler im Zweig des Ladepunkts. - """ - self.connected_counters.clear() - self.__get_all_counter_in_branch(self.data.get.hierarchy[0], num) - return self.connected_counters - - def get_entry_of_element(self, id_to_find: int) -> Dict: - item = self.__is_id_in_top_level(id_to_find) - if item: - return item - else: - return self.__get_entry(self.data.get.hierarchy[0], id_to_find, self.__get_entry_of_element) - - def get_entry_of_parent(self, id_to_find: int) -> Dict: - if self.__is_id_in_top_level(id_to_find): - return {} - for child in self.data.get.hierarchy[0]["children"]: - if child["id"] == id_to_find: - return self.data.get.hierarchy[0] - else: - return self.__get_entry(self.data.get.hierarchy[0], id_to_find, self.__get_entry_of_parent) - - def __is_id_in_top_level(self, id_to_find: int) -> Dict: - for item in self.data.get.hierarchy: - if item["id"] == id_to_find: - return item - else: - return {} - - def __get_all_counter_in_branch(self, child: Dict, id_to_find: int) -> bool: - """ Rekursive Funktion, die alle Zweige durchgeht, bis der entsprechende Ladepunkt gefunden wird und dann alle - Zähler in diesem Pfad der Liste anhängt. - """ - parent_id = child["id"] - for child in child["children"]: - if child["id"] == id_to_find: - self.connected_counters.append(f"counter{parent_id}") - return True - if len(child["children"]) != 0: - found = self.__get_all_counter_in_branch(child, id_to_find) - if found: - self.connected_counters.append(f"counter{parent_id}") - return True - else: - return False - - def __get_entry(self, child: Dict, id_to_find: int, func: Callable[[Dict, int], bool]) -> Dict: - for child in child["children"]: - found = func(child, id_to_find) - if found: - return child - if len(child["children"]) != 0: - entry = self.__get_entry(child, id_to_find, func) - if entry: - return entry - else: - return {} - - def __get_entry_of_element(self, child: Dict, id_to_find: int) -> bool: - if child["id"] == id_to_find: - return True - else: - return False - - def __get_entry_of_parent(self, child: Dict, id_to_find: int) -> bool: - for child2 in child["children"]: - if child2["id"] == id_to_find: - return True - else: - return False - - def hierarchy_add_item_aside(self, new_id: int, new_type: ComponentType, id_to_find: int) -> None: - """ ruft die rekursive Funktion zum Hinzufügen eines Zählers oder Ladepunkts in die Zählerhierarchie auf - derselben Ebene wie das angegebene Element. - """ - if self.__is_id_in_top_level(id_to_find): - self.data.get.hierarchy.append({"id": new_id, "type": new_type.value, "children": []}) - else: - if (self.__edit_element_in_hierarchy( - self.data.get.hierarchy[0], - id_to_find, self._add_item_aside, new_id, new_type) is False): - raise IndexError(f"Element {id_to_find} konnte nicht in der Hierarchie gefunden werden.") - - def _add_item_aside( - self, child: Dict, current_entry: Dict, id_to_find: int, new_id: int, new_type: ComponentType) -> bool: - if id_to_find == child["id"]: - current_entry["children"].append({"id": new_id, "type": new_type.value, "children": []}) - return True - else: - return False - - def hierarchy_remove_item(self, id_to_find: int, keep_children: bool = True) -> None: - """ruft die rekursive Funktion zum Löschen eines Elements. Je nach Flag werden die Kinder gelöscht oder auf die - Ebene des gelöschten Elements gehoben. - """ - item = self.__is_id_in_top_level(id_to_find) - if item: - if keep_children: - self.data.get.hierarchy.extend(item["children"]) - self.data.get.hierarchy.remove(item) - else: - if (self.__edit_element_in_hierarchy( - self.data.get.hierarchy[0], - id_to_find, self._remove_item, keep_children) is False): - raise IndexError(f"Element {id_to_find} konnte nicht in der Hierarchie gefunden werden.") - - def _remove_item(self, child: Dict, current_entry: Dict, id: str, keep_children: bool) -> bool: - if id == child["id"]: - if keep_children: - current_entry["children"].extend(child["children"]) - current_entry["children"].remove(child) - return True - else: - return False - - def hierarchy_add_item_below_evu(self, new_id: int, new_type: ComponentType) -> None: - try: - self.hierarchy_add_item_below(new_id, new_type, self.get_id_evu_counter()) - except (TypeError, IndexError): - if new_type == ComponentType.COUNTER: - # es gibt noch keinen EVU-Zähler - hierarchy = [{ - "id": new_id, - "type": ComponentType.COUNTER.value, - "children": self.data.get.hierarchy - }] - self.data.get.hierarchy = hierarchy - else: - raise ValueError(self.MISSING_EVU_COUNTER) - - def hierarchy_add_item_below(self, new_id: int, new_type: ComponentType, id_to_find: int) -> None: - """ruft die rekursive Funktion zum Hinzufügen eines Elements als Kind des angegebenen Elements. - """ - item = self.__is_id_in_top_level(id_to_find) - if item: - item["children"].append({"id": new_id, "type": new_type.value, "children": []}) - else: - if (self.__edit_element_in_hierarchy( - self.data.get.hierarchy[0], - id_to_find, self._add_item_below, new_id, new_type) is False): - raise IndexError(f"Element {id_to_find} konnte nicht in der Hierarchie gefunden werden.") - - def _add_item_below( - self, child: Dict, current_entry: Dict, id_to_find: int, new_id: int, new_type: ComponentType) -> bool: - if id_to_find == child["id"]: - child["children"].append({"id": new_id, "type": new_type.value, "children": []}) - return True - else: - return False - - def __edit_element_in_hierarchy(self, current_entry: Dict, id_to_find: int, func: Callable, *args) -> bool: - for child in current_entry["children"]: - if func(child, current_entry, id_to_find, *args): - return True - else: - if len(child["children"]) != 0: - if self.__edit_element_in_hierarchy(child, id_to_find, func, *args): - return True - else: - return False - - def get_list_of_elements_per_level(self) -> List[List[Dict[str, Union[int, str]]]]: - elements_per_level: List[List[Dict[str, Union[int, str]]]] = [] - for item in self.data.get.hierarchy: - list(zip(elements_per_level, self._get_list_of_elements_per_level(elements_per_level, item, 0))) - return elements_per_level - - def _get_list_of_elements_per_level(self, elements_per_level: List, child: Dict, index: int) -> List: - try: - elements_per_level[index].extend([{"type": child["type"], "id": child["id"]}]) - except IndexError: - elements_per_level.insert(index, [{"type": child["type"], "id": child["id"]}]) - for child in child["children"]: - elements_per_level = self._get_list_of_elements_per_level(elements_per_level, child, index+1) - return elements_per_level - - def validate_hierarchy(self): - try: - self._delete_obsolete_entries() - self._add_missing_entries() - except Exception: - log.exception("Fehler bei der Validierung der Hierarchie") - - def _delete_obsolete_entries(self): - def check_and_remove(name, type_name: ComponentType, data_structure): - if element["type"] == type_name.value: - if f"{name}{element['id']}" not in data_structure: - self.hierarchy_remove_item(element["id"]) - pub_system_message({}, f"{component_type_to_readable_text(type_name)} mit ID {element['id']} wurde" - " aus der Hierarchie entfernt, da keine gültige Konfiguration gefunden wurde.", - MessageType.WARNING) - - for level in self.get_list_of_elements_per_level(): - for element in level: - check_and_remove("bat", ComponentType.BAT, data.data.bat_data) - check_and_remove("counter", ComponentType.COUNTER, data.data.counter_data) - check_and_remove("cp", ComponentType.CHARGEPOINT, data.data.cp_data) - check_and_remove("pv", ComponentType.INVERTER, data.data.pv_data) - - def _add_missing_entries(self): - def check_and_add(type_name: ComponentType, data_structure): - for entry in data_structure: - break_flag = False - re_result = re.search("[0-9]+", entry) - if re_result is not None: - entry_num = int(re_result.group()) - for level in self.get_list_of_elements_per_level(): - for element in level: - if entry_num == element["id"] and element["type"] == type_name.value: - break_flag = True - break - if break_flag: - break - else: - try: - self.hierarchy_add_item_below_evu(entry_num, type_name) - except ValueError: - pub_system_message({}, "Die Struktur des Lastmanagements ist nicht plausibel. Bitte prüfe die " - "Konfiguration und Anordnung der Komponenten in der Hierarchie.", - MessageType.WARNING) - - pub_system_message({}, f"{component_type_to_readable_text(type_name)} mit ID {element['id']} wurde" - " in der Struktur des Lastmanagements hinzugefügt, da kein Eintrag in der " - "Struktur gefunden wurde. Bitte prüfe die Anordnung der Komponenten in der " - "Struktur.", - MessageType.WARNING) - - # Falls EVU-Zähler fehlt, zuerst hinzufügen. - check_and_add(ComponentType.COUNTER, data.data.counter_data) - try: - self.get_id_evu_counter() - check_and_add(ComponentType.BAT, data.data.bat_data) - check_and_add(ComponentType.CHARGEPOINT, data.data.cp_data) - check_and_add(ComponentType.INVERTER, data.data.pv_data) - except TypeError: - pub_system_message({}, ("Es konnte kein Zähler gefunden werden, der als EVU-Zähler an die Spitze des " - "Lastmanagements gesetzt werden kann. Bitte zuerst einen EVU-Zähler hinzufügen."), - MessageType.ERROR) - - -def get_max_id_in_hierarchy(current_entry: List, max_id: int) -> int: - for item in current_entry: - if item["id"] > max_id: - max_id = item["id"] - if len(item["children"]) != 0: - max_id = get_max_id_in_hierarchy(item["children"], max_id) - else: - return max_id - - -def get_counter_default_config(): - return {"max_currents": [16, 16, 16], - "max_total_power": 11000} diff --git a/packages/control/counter_all/counter_all.py b/packages/control/counter_all/counter_all.py new file mode 100644 index 0000000000..375857a426 --- /dev/null +++ b/packages/control/counter_all/counter_all.py @@ -0,0 +1,152 @@ +"""Zähler-Logik +""" +import copy +import logging +from typing import List, Tuple + +from control import data +from control.counter import Counter +from control.counter_all.counter_all_data import CounterAllData +from control.counter_all.hierarchy import HierarchyMixin +from control.counter_all.loadmanagement_prio import LoadmanagementPrioMixin +from helpermodules.pub import Pub +from modules.common.component_type import ComponentType +from modules.common.fault_state import FaultStateLevel +from modules.common.simcount import SimCounter + +log = logging.getLogger(__name__) + + +class CounterAll(HierarchyMixin, LoadmanagementPrioMixin): + MISSING_EVU_COUNTER = "Bitte erst einen EVU-Zähler konfigurieren." + + def __init__(self): + self.data = CounterAllData() + self.sim_counter = SimCounter("", "", prefix="bezug") + self.sim_counter.topic = "openWB/set/counter/set/" + + def get_evu_counter(self) -> Counter: + return data.data.counter_data[f"counter{self.get_id_evu_counter()}"] + + def get_evu_counter_str(self) -> str: + return f"counter{self.get_id_evu_counter()}" + + def get_id_evu_counter(self) -> int: + try: + if ComponentType.COUNTER.value == self.data.get.hierarchy[0]["type"]: + return self.data.get.hierarchy[0]['id'] + else: + raise TypeError + except Exception: + log.error( + "Ohne Konfiguration eines EVU-Zählers an der Spitze der Hierarchie ist keine Regelung und keine Ladung " + "möglich.") + raise + + def set_home_consumption(self) -> None: + try: + self._validate_home_consumption_counter() + home_consumption, elements = self._calc_home_consumption() + if home_consumption < 0: + log.error( + f"Ungültiger Hausverbrauch: {home_consumption}W, Berücksichtigte Komponenten neben EVU {elements}") + if self.data.config.home_consumption_source_id is None: + hc_counter_source = self.get_evu_counter_str() + else: + hc_counter_source = f"counter{self.data.config.home_consumption_source_id}" + hc_counter_data = data.data.counter_data[hc_counter_source].data + if hc_counter_data.get.fault_state == FaultStateLevel.NO_ERROR: + hc_counter_data.get.fault_state = FaultStateLevel.WARNING.value + hc_counter_data.get.fault_str = ("Hinweis: Es gibt mehr Stromerzeuger im Haus als in der openWB " + "eingetragen sind. Der Hausverbrauch kann nicht korrekt berechnet " + "werden. Dies hat auf die PV-Überschussladung keine negativen " + "Auswirkungen.") + if self.data.set.invalid_home_consumption < 3: + self.data.set.invalid_home_consumption += 1 + return + else: + home_consumption = 0 + else: + self.data.set.invalid_home_consumption = 0 + self.data.set.home_consumption = home_consumption + imported, _ = self.sim_counter.sim_count(self.data.set.home_consumption) + self.data.set.imported_home_consumption = imported + except Exception: + log.exception("Fehler in der allgemeinen Zähler-Klasse") + + EVU_IS_HC_COUNTER_ERROR = ("Der EVU-Zähler kann nicht als Quelle für den Hausverbrauch verwendet werden. Meist ist " + "der Zähler am EVU-Punkt installiert, dann muss im Lastmanagement unter Hausverbrauch" + " 'von openWB berechnen' ausgewählt werden. Wenn der Zähler im Hausverbrauchszweig " + "installiert ist, einen virtuellen Zähler anlegen und im Lastmanagement ganz links " + "anordnen.") + + def _validate_home_consumption_counter(self): + if self.data.config.home_consumption_source_id is not None: + if self.data.config.home_consumption_source_id == self.get_id_evu_counter(): + hc_counter_data = data.data.counter_data[self.get_evu_counter_str()].data + hc_counter_data.get.fault_state = FaultStateLevel.ERROR.value + hc_counter_data.get.fault_str = self.EVU_IS_HC_COUNTER_ERROR + evu_counter = self.get_id_evu_counter() + Pub().pub(f"openWB/set/counter/{evu_counter}/get/fault_state", + hc_counter_data.get.fault_state) + Pub().pub(f"openWB/set/counter/{evu_counter}/get/fault_str", + hc_counter_data.get.fault_str) + raise Exception(self.EVU_IS_HC_COUNTER_ERROR) + + def _calc_home_consumption(self) -> Tuple[float, List]: + power = 0 + if self.data.config.home_consumption_source_id is None: + id_source = self.get_id_evu_counter() + else: + id_source = self.data.config.home_consumption_source_id + elements_to_sum_up = self.get_elements_for_downstream_calculation(id_source) + for element in elements_to_sum_up: + if element["type"] == ComponentType.CHARGEPOINT.value: + component = data.data.cp_data[f"cp{element['id']}"] + elif element["type"] == ComponentType.BAT.value: + component = data.data.bat_data[f"bat{element['id']}"] + elif element["type"] == ComponentType.COUNTER.value: + component = data.data.counter_data[f"counter{element['id']}"] + elif element["type"] == ComponentType.INVERTER.value: + component = data.data.pv_data[f"pv{element['id']}"] + + if component.data.get.fault_state < 2: + power += component.data.get.power + else: + log.warning( + f"Komponente {element['type']}{component.num} ist im Fehlerzustand und wird nicht berücksichtigt.") + evu = data.data.counter_data[f"counter{id_source}"].data.get.power + return evu - power - self.data.set.smarthome_power_excluded_from_home_consumption, elements_to_sum_up + + def _add_hybrid_bat(self, id: int) -> List: + elements = [] + inverter_children = self.get_entry_of_element(id)["children"] + for child in inverter_children: + if child["type"] == ComponentType.BAT.value: + elements.append(child) + return elements + + def get_elements_for_downstream_calculation(self, id: int): + """returns a list of elements that are relevant for the calculation of the counter values based on the + downstream components, eg home consumption or virtual counter.""" + elements = copy.deepcopy(self.get_entry_of_element(id)["children"]) + elements_to_sum_up = elements + for element in elements: + if element["type"] == ComponentType.INVERTER.value: + elements_to_sum_up.extend(self._add_hybrid_bat(element['id'])) + return elements_to_sum_up + + +def get_max_id_in_hierarchy(current_entry: List, max_id: int) -> int: + for item in current_entry: + if item["id"] > max_id: + max_id = item["id"] + if len(item["children"]) != 0: + max_id = get_max_id_in_hierarchy(item["children"], max_id) + else: + return max_id + + +def get_counter_default_config(): + return {"max_currents": [16, 16, 16], + "max_total_power": 11000} diff --git a/packages/control/counter_all/counter_all_data.py b/packages/control/counter_all/counter_all_data.py new file mode 100644 index 0000000000..c137c5add5 --- /dev/null +++ b/packages/control/counter_all/counter_all_data.py @@ -0,0 +1,109 @@ +from dataclasses import dataclass, field +from typing import Callable, Dict, Generator, List, Optional, Protocol, Tuple, Union + +from control.chargepoint.chargepoint import Chargepoint +from dataclass_utils.factories import empty_list_factory +from modules.common.component_type import ComponentType + + +@dataclass +class Config: + home_consumption_source_id: Optional[str] = field( + default=None, metadata={"topic": "config/home_consumption_source_id"}) + consider_less_charging: bool = field( + default=False, metadata={"topic": "config/consider_less_charging"}) + + +def config_factory() -> Config: + return Config() + + +@dataclass +class Set: + loadmanagement_active: bool = field( + default=False, metadata={"topic": "set/loadmanagement_active"}) + home_consumption: float = field(default=0, metadata={"topic": "set/home_consumption"}) + smarthome_power_excluded_from_home_consumption: float = field( + default=0, + metadata={"topic": "set/smarthome_power_excluded_from_home_consumption"}) + invalid_home_consumption: int = field( + default=0, metadata={"topic": "set/invalid_home_consumption"}) + daily_yield_home_consumption: float = field( + default=0, metadata={"topic": "set/daily_yield_home_consumption"}) + imported_home_consumption: float = field( + default=0, metadata={"topic": "set/imported_home_consumption"}) + disengageable_smarthome_power: float = field( + default=0, metadata={"topic": "set/disengageable_smarthome_power"}) + + +@dataclass +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: + return Get() + + +def set_factory() -> Set: + return Set() + + +@dataclass +class CounterAllData: + config: Config = field(default_factory=config_factory) + get: Get = field(default_factory=get_factory) + set: Set = field(default_factory=set_factory) + + +class HierarchyProtocol(Protocol): + @property + def childless(self) -> List: ... + @property + def connected_chargepoints(self) -> List: ... + @property + def connected_counters(self) -> List: ... + @property + def data(self) -> CounterAllData: ... + @property + def MISSING_EVU_COUNTER(self) -> str: ... + + def _add_item_aside(self, child: Dict, current_entry: Dict, id_to_find: int, + new_id: int, new_type: ComponentType) -> bool: ... + def _add_item_below(self, child: Dict, current_entry: Dict, id_to_find: int, + new_id: int, new_type: ComponentType) -> bool: ... + + def _add_missing_entries(self): ... + def _delete_obsolete_entries(self): ... + def _edit_element_in_hierarchy(self, current_entry: Dict, id_to_find: int, func: Callable, *args) -> bool: ... + def _get_all_counter_in_branch(self, child: Dict, id_to_find: int) -> bool: ... + def _get_all_cp_connected_to_counter(self, child: Dict) -> None: ... + def _get_all_elements_without_children_recursive(self, child: Dict) -> None: ... + def _get_entry(self, child: Dict, id_to_find: int, func: Callable[[Dict, int], bool]) -> Dict: ... + def _get_entry_of_element(self, child: Dict, id_to_find: int) -> bool: ... + def _get_entry_of_parent(self, child: Dict, id_to_find: int) -> bool: ... + def _get_list_of_elements_per_level(self, elements_per_level: List, child: Dict, index: int) -> List: ... + def _is_id_in_top_level(self, id_to_find: int) -> Dict: ... + def _remove_item(self, child: Dict, current_entry: Dict, id: str, keep_children: bool) -> bool: ... + def get_entry_of_element(self, id: int) -> Dict: ... + def get_evu_counter_str(self) -> str: ... + def get_id_evu_counter(self) -> int: ... + def get_list_of_elements_per_level(self) -> List[List[Dict[str, Union[int, str]]]]: ... + def hierarchy_add_item_below(self, new_id: int, new_type: ComponentType, id_to_find: int) -> None: ... + def hierarchy_add_item_below_evu(self, new_id: int, new_type: ComponentType) -> None: ... + def hierarchy_remove_item(self, id_to_find: int, keep_children: bool = True) -> None: ... + + +class LoadmanagementPrioProtocol(Protocol): + @property + def data(self) -> CounterAllData: ... + def add_loadmanagement_prio_item(self, type: str, id: int) -> None: ... + def remove_loadmanagement_prio_item(self, id: int) -> None: ... + def _remove_loadmanagement_prio_item(self, id: int, entry: Dict) -> None: ... + def sort_cps_by_loadmanagement_prios_nested(self, filtered_cps: List[Chargepoint]) -> List[List[Chargepoint]]: ... + + def generator_cps_by_loadmanagement_prios( + self, + filtered_cps: List[Chargepoint]) -> Generator[Tuple[Chargepoint, List[Chargepoint]], None, None]: ... diff --git a/packages/control/counter_home_consumption_test.py b/packages/control/counter_all/counter_home_consumption_test.py similarity index 98% rename from packages/control/counter_home_consumption_test.py rename to packages/control/counter_all/counter_home_consumption_test.py index 6303dd63e9..034645a50d 100644 --- a/packages/control/counter_home_consumption_test.py +++ b/packages/control/counter_all/counter_home_consumption_test.py @@ -4,7 +4,7 @@ from control import data from packages.conftest import hierarchy_hc_counter, hierarchy_standard, hierarchy_hybrid, hierarchy_nested -from control.counter_all import CounterAll +from control.counter_all.counter_all import CounterAll from modules.common.fault_state import FaultStateLevel diff --git a/packages/control/counter_all/hierarchy.py b/packages/control/counter_all/hierarchy.py new file mode 100644 index 0000000000..bddddd8a4b --- /dev/null +++ b/packages/control/counter_all/hierarchy.py @@ -0,0 +1,317 @@ +import logging +import re +from typing import Callable, Dict, List, Union + +from control import data +from control.counter_all.counter_all_data import HierarchyProtocol +from helpermodules.messaging import MessageType, pub_system_message +from modules.common.component_type import ComponentType, component_type_to_readable_text + +log = logging.getLogger(__name__) + + +class HierarchyMixin: + def get_all_elements_without_children(self: HierarchyProtocol, id: int) -> List[Dict]: + self.childless = [] + self._get_all_elements_without_children_recursive(self.get_entry_of_element(id)) + return self.childless + + def _get_all_elements_without_children_recursive(self: HierarchyProtocol, child: Dict) -> None: + for child in child["children"]: + try: + if len(child["children"]) != 0: + self._get_all_elements_without_children_recursive(child) + else: + self.childless.append(child) + except Exception: + log.exception("Fehler in der allgemeinen Zähler-Klasse") + + def get_chargepoints_of_counter(self: HierarchyProtocol, counter: str) -> List[str]: + """ gibt eine Liste der Ladepunkte, die in den folgenden Zweigen des Zählers sind, zurück. + """ + self.connected_chargepoints = [] + if counter == self.get_evu_counter_str(): + counter_object = self.data.get.hierarchy[0] + else: + counter_object = self._get_entry( + self.data.get.hierarchy[0], + int(counter[7:]), + self._get_entry_of_element) + try: + self._get_all_cp_connected_to_counter(counter_object) + except KeyError: + # Kein Ladepunkt unter dem Zähler + pass + return self.connected_chargepoints + + def _get_all_cp_connected_to_counter(self: HierarchyProtocol, child: Dict) -> None: + """ Rekursive Funktion, die alle Ladepunkte ermittelt, die an den angegebenen Zähler angeschlossen sind. + """ + # Alle Objekte der Ebene durchgehen + for child in child["children"]: + try: + if child["type"] == ComponentType.CHARGEPOINT.value: + self.connected_chargepoints.append(f"cp{child['id']}") + # Wenn das Objekt noch Kinder hat, diese ebenfalls untersuchen. + elif len(child["children"]) != 0: + self._get_all_cp_connected_to_counter(child) + except Exception: + log.exception("Fehler in der allgemeinen Zähler-Klasse") + + def get_counters_to_check(self: HierarchyProtocol, num: int) -> List[str]: + """ ermittelt alle Zähler im Zweig des Ladepunkts. + """ + self.connected_counters = [] + self._get_all_counter_in_branch(self.data.get.hierarchy[0], num) + return self.connected_counters + + def get_entry_of_element(self: HierarchyProtocol, id_to_find: int) -> Dict: + item = self._is_id_in_top_level(id_to_find) + if item: + return item + else: + return self._get_entry(self.data.get.hierarchy[0], id_to_find, self._get_entry_of_element) + + def get_entry_of_parent(self: HierarchyProtocol, id_to_find: int) -> Dict: + if self._is_id_in_top_level(id_to_find): + return {} + for child in self.data.get.hierarchy[0]["children"]: + if child["id"] == id_to_find: + return self.data.get.hierarchy[0] + else: + return self._get_entry(self.data.get.hierarchy[0], id_to_find, self._get_entry_of_parent) + + def _is_id_in_top_level(self: HierarchyProtocol, id_to_find: int) -> Dict: + for item in self.data.get.hierarchy: + if item["id"] == id_to_find: + return item + else: + return {} + + def _get_all_counter_in_branch(self: HierarchyProtocol, child: Dict, id_to_find: int) -> bool: + """ Rekursive Funktion, die alle Zweige durchgeht, bis der entsprechende Ladepunkt gefunden wird und dann alle + Zähler in diesem Pfad der Liste anhängt. + """ + parent_id = child["id"] + for child in child["children"]: + if child["id"] == id_to_find: + self.connected_counters.append(f"counter{parent_id}") + return True + if len(child["children"]) != 0: + found = self._get_all_counter_in_branch(child, id_to_find) + if found: + self.connected_counters.append(f"counter{parent_id}") + return True + else: + return False + + def _get_entry(self: HierarchyProtocol, child: Dict, id_to_find: int, func: Callable[[Dict, int], bool]) -> Dict: + for child in child["children"]: + found = func(child, id_to_find) + if found: + return child + if len(child["children"]) != 0: + entry = self._get_entry(child, id_to_find, func) + if entry: + return entry + else: + return {} + + def _get_entry_of_element(self: HierarchyProtocol, child: Dict, id_to_find: int) -> bool: + if child["id"] == id_to_find: + return True + else: + return False + + def _get_entry_of_parent(self: HierarchyProtocol, child: Dict, id_to_find: int) -> bool: + for child2 in child["children"]: + if child2["id"] == id_to_find: + return True + else: + return False + + def hierarchy_add_item_aside(self: HierarchyProtocol, + new_id: int, new_type: ComponentType, + id_to_find: int) -> None: + """ ruft die rekursive Funktion zum Hinzufügen eines Zählers oder Ladepunkts in die Zählerhierarchie auf + derselben Ebene wie das angegebene Element. + """ + if self._is_id_in_top_level(id_to_find): + self.data.get.hierarchy.append({"id": new_id, "type": new_type.value, "children": []}) + else: + if (self._edit_element_in_hierarchy( + self.data.get.hierarchy[0], + id_to_find, self._add_item_aside, new_id, new_type) is False): + raise IndexError(f"Element {id_to_find} konnte nicht in der Hierarchie gefunden werden.") + + def _add_item_aside(self: HierarchyProtocol, + child: Dict, + current_entry: List, + id_to_find: int, + new_id: int, + new_type: ComponentType) -> bool: + if id_to_find == child["id"]: + current_entry["children"].append({"id": new_id, "type": new_type.value, "children": []}) + return True + else: + return False + + def hierarchy_remove_item(self: HierarchyProtocol, id_to_find: int, keep_children: bool = True) -> None: + """ruft die rekursive Funktion zum Löschen eines Elements. Je nach Flag werden die Kinder gelöscht oder auf die + Ebene des gelöschten Elements gehoben. + """ + item = self._is_id_in_top_level(id_to_find) + if item: + if keep_children: + self.data.get.hierarchy.extend(item["children"]) + self.data.get.hierarchy.remove(item) + else: + if (self._edit_element_in_hierarchy( + self.data.get.hierarchy[0], + id_to_find, self._remove_item, keep_children) is False): + raise IndexError(f"Element {id_to_find} konnte nicht in der Hierarchie gefunden werden.") + + def _remove_item(self: HierarchyProtocol, child: Dict, current_entry: Dict, id: str, keep_children: bool) -> bool: + if id == child["id"]: + if keep_children: + current_entry["children"].extend(child["children"]) + current_entry["children"].remove(child) + return True + else: + return False + + def hierarchy_add_item_below_evu(self: HierarchyProtocol, new_id: int, new_type: ComponentType) -> None: + try: + self.hierarchy_add_item_below(new_id, new_type, self.get_id_evu_counter()) + except (TypeError, IndexError): + if new_type == ComponentType.COUNTER: + # es gibt noch keinen EVU-Zähler + hierarchy = [{ + "id": new_id, + "type": ComponentType.COUNTER.value, + "children": self.data.get.hierarchy + }] + self.data.get.hierarchy = hierarchy + else: + raise ValueError(self.MISSING_EVU_COUNTER) + + def hierarchy_add_item_below(self: HierarchyProtocol, + new_id: int, + new_type: ComponentType, id_to_find: int) -> None: + """ruft die rekursive Funktion zum Hinzufügen eines Elements als Kind des angegebenen Elements. + """ + item = self._is_id_in_top_level(id_to_find) + if item: + item["children"].append({"id": new_id, "type": new_type.value, "children": []}) + else: + if (self._edit_element_in_hierarchy( + self.data.get.hierarchy[0], + id_to_find, self._add_item_below, new_id, new_type) is False): + raise IndexError(f"Element {id_to_find} konnte nicht in der Hierarchie gefunden werden.") + + def _add_item_below(self: HierarchyProtocol, + child: Dict, current_entry: Dict, + id_to_find: int, + new_id: int, + new_type: ComponentType) -> bool: + if id_to_find == child["id"]: + child["children"].append({"id": new_id, "type": new_type.value, "children": []}) + return True + else: + return False + + def _edit_element_in_hierarchy(self: HierarchyProtocol, + current_entry: Dict, + id_to_find: int, + func: Callable, + *args) -> bool: + for child in current_entry["children"]: + if func(child, current_entry, id_to_find, *args): + return True + else: + if len(child["children"]) != 0: + if self._edit_element_in_hierarchy(child, id_to_find, func, *args): + return True + else: + return False + + def get_list_of_elements_per_level(self: HierarchyProtocol) -> List[List[Dict[str, Union[int, str]]]]: + elements_per_level: List[List[Dict[str, Union[int, str]]]] = [] + for item in self.data.get.hierarchy: + list(zip(elements_per_level, self._get_list_of_elements_per_level(elements_per_level, item, 0))) + return elements_per_level + + def _get_list_of_elements_per_level(self: HierarchyProtocol, + elements_per_level: List[List[Dict[str, Union[int, str]]]], + child: Dict, + index: int) -> List: + try: + elements_per_level[index].extend([{"type": child["type"], "id": child["id"]}]) + except IndexError: + elements_per_level.insert(index, [{"type": child["type"], "id": child["id"]}]) + for child in child["children"]: + elements_per_level = self._get_list_of_elements_per_level(elements_per_level, child, index+1) + return elements_per_level + + def validate_hierarchy(self: HierarchyProtocol): + try: + self._delete_obsolete_entries() + self._add_missing_entries() + except Exception: + log.exception("Fehler bei der Validierung der Hierarchie") + + def _delete_obsolete_entries(self: HierarchyProtocol): + def check_and_remove(name, type_name: ComponentType, data_structure): + if element["type"] == type_name.value: + if f"{name}{element['id']}" not in data_structure: + self.hierarchy_remove_item(element["id"]) + pub_system_message({}, f"{component_type_to_readable_text(type_name)} mit ID {element['id']} wurde" + " aus der Hierarchie entfernt, da keine gültige Konfiguration gefunden wurde.", + MessageType.WARNING) + + for level in self.get_list_of_elements_per_level(): + for element in level: + check_and_remove("bat", ComponentType.BAT, data.data.bat_data) + check_and_remove("counter", ComponentType.COUNTER, data.data.counter_data) + check_and_remove("cp", ComponentType.CHARGEPOINT, data.data.cp_data) + check_and_remove("pv", ComponentType.INVERTER, data.data.pv_data) + + def _add_missing_entries(self: HierarchyProtocol): + def check_and_add(type_name: ComponentType, data_structure): + for entry in data_structure: + break_flag = False + re_result = re.search("[0-9]+", entry) + if re_result is not None: + entry_num = int(re_result.group()) + for level in self.get_list_of_elements_per_level(): + for element in level: + if entry_num == element["id"] and element["type"] == type_name.value: + break_flag = True + break + if break_flag: + break + else: + try: + self.hierarchy_add_item_below_evu(entry_num, type_name) + except ValueError: + pub_system_message({}, "Die Struktur des Lastmanagements ist nicht plausibel. Bitte prüfe die " + "Konfiguration und Anordnung der Komponenten in der Hierarchie.", + MessageType.WARNING) + + pub_system_message({}, f"{component_type_to_readable_text(type_name)} mit ID {element['id']} wurde" + " in der Struktur des Lastmanagements hinzugefügt, da kein Eintrag in der " + "Struktur gefunden wurde. Bitte prüfe die Anordnung der Komponenten in der " + "Struktur.", + MessageType.WARNING) + + # Falls EVU-Zähler fehlt, zuerst hinzufügen. + check_and_add(ComponentType.COUNTER, data.data.counter_data) + try: + self.get_id_evu_counter() + check_and_add(ComponentType.BAT, data.data.bat_data) + check_and_add(ComponentType.CHARGEPOINT, data.data.cp_data) + check_and_add(ComponentType.INVERTER, data.data.pv_data) + except TypeError: + pub_system_message({}, ("Es konnte kein Zähler gefunden werden, der als EVU-Zähler an die Spitze des " + "Lastmanagements gesetzt werden kann. Bitte zuerst einen EVU-Zähler hinzufügen."), + MessageType.ERROR) 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..20805a2309 100644 --- a/packages/control/hierarchy_test.py +++ b/packages/control/counter_all/hierarchy_test.py @@ -5,7 +5,7 @@ from control.counter import Counter -from control.counter_all import CounterAll, get_max_id_in_hierarchy +from control.counter_all.counter_all import CounterAll, get_max_id_in_hierarchy from modules.common.component_type import ComponentType diff --git a/packages/control/counter_all/loadmanagement_prio.py b/packages/control/counter_all/loadmanagement_prio.py new file mode 100644 index 0000000000..2ac9cbc403 --- /dev/null +++ b/packages/control/counter_all/loadmanagement_prio.py @@ -0,0 +1,87 @@ +import logging +from typing import Dict, Generator, List, Tuple + +from control.algorithm.filter_chargepoints import get_preferenced_chargepoint +from control.chargepoint.chargepoint import Chargepoint +from control.counter_all.counter_all_data import LoadmanagementPrioProtocol + + +log = logging.getLogger(__name__) + + +class LoadmanagementPrioMixin: + def add_loadmanagement_prio_item(self: LoadmanagementPrioProtocol, type: str, id: int) -> None: + self.data.get.loadmanagement_prios.append({"type": type, "id": id}) + + def remove_loadmanagement_prio_item(self: LoadmanagementPrioProtocol, id: int) -> None: + if self._remove_loadmanagement_prio_item(id, self.data.get.loadmanagement_prios) is False: + raise IndexError(f"Element {id} konnte nicht in der Prioritätensteuerung gefunden werden.") + + def _remove_loadmanagement_prio_item(self: LoadmanagementPrioProtocol, id: int, entry: List[Dict]) -> bool: + for item in entry: + if item["type"] == "vehicle": + if item["id"] == id: + entry.remove(item) + return True + elif item["type"] == "group": + removed_item = self._remove_loadmanagement_prio_item(id, item["children"]) + if removed_item and len(item["children"]) == 0: + entry.remove(item) + if removed_item: + return True + return False + + def generator_cps_by_loadmanagement_prios( + self: LoadmanagementPrioProtocol, + filtered_cps: List[Chargepoint]) -> Generator[Tuple[Chargepoint, List[Chargepoint]], None, None]: + sorted_cps = self.sort_cps_by_loadmanagement_prios_nested(filtered_cps) + log.debug("Ladepunkte sortiert nach Prioritätensteuerung: ") + log.debug([[f"LP {cp.num}" for cp in group] for group in sorted_cps]) + for group in sorted_cps: + cp: Chargepoint + while len(group) > 0: + cp = group[0] + yield cp, group.copy() + group.pop(0) + + def sort_cps_by_loadmanagement_prios_nested(self: LoadmanagementPrioProtocol, + filtered_cps: List[Chargepoint]) -> List[List[Chargepoint]]: + sorted_cps = [] + for entry in self.data.get.loadmanagement_prios: + if entry["type"] == "vehicle": + grouped_cps = [] + for cp in filtered_cps: + if cp.data.config.ev == entry["id"]: + grouped_cps.append(cp) + if len(grouped_cps) > 0: + sorted_grouped_cps = get_preferenced_chargepoint(grouped_cps) + sorted_cps.append(sorted_grouped_cps) + elif entry["type"] == "group": + sorted_grouped_cps = [] + for group_entry in entry["children"]: + grouped_cps = [] + for cp in filtered_cps: + if cp.data.config.ev == group_entry["id"]: + grouped_cps.append(cp) + sorted_grouped_cps.extend(get_preferenced_chargepoint(grouped_cps)) + if len(sorted_grouped_cps) > 0: + sorted_cps.append(sorted_grouped_cps) + return sorted_cps + + # def sort_cps_by_loadmanagement_prios_flat(self: LoadmanagementPrioProtocol, + # filtered_cps: List[Chargepoint]) -> List[Chargepoint]: + # sorted_cps = [] + # for entry in self.data.get.loadmanagement_prios: + # if entry["type"] == "vehicle": + # for cp in filtered_cps: + # if cp.data.config.ev == entry["id"]: + # sorted_cps.append(cp) + # elif entry["type"] == "group": + # for group_entry in entry["children"]: + # for cp in filtered_cps: + # if cp.data.config.ev == group_entry["id"]: + # sorted_cps.append(cp) + # if len(sorted_cps) != len(filtered_cps): + # raise ValueError( + # "Fahrzeuge der Prioritätensteuerung konnten nicht korrekt den Ladepunkten zugeordnet werden.") + # return sorted_cps diff --git a/packages/control/counter_all/loadmanagement_prio_test.py b/packages/control/counter_all/loadmanagement_prio_test.py new file mode 100644 index 0000000000..f6a7c01433 --- /dev/null +++ b/packages/control/counter_all/loadmanagement_prio_test.py @@ -0,0 +1,265 @@ + +from typing import Dict, List +from unittest.mock import Mock + +import pytest + +from control.chargepoint.chargepoint import Chargepoint +from control.counter_all.counter_all import CounterAll + + +@pytest.fixture +def cp1(): + cp = Chargepoint(1, None) + cp.data.control_parameter.required_current = 8 + return cp + + +@pytest.fixture +def cp2(): + cp = Chargepoint(2, None) + cp.data.control_parameter.required_current = 7 + return cp + + +@pytest.fixture +def cp3(): + cp = Chargepoint(3, None) + cp.data.control_parameter.required_current = 6 + return cp + + +@pytest.mark.parametrize( + "loadmanagement_prios, id, type, expected_loadmanagement_prios", + [ + pytest.param([], 2, "vehicle", [{"type": "vehicle", "id": 2}], id="emtpy list"), + pytest.param([{"type": "vehicle", "id": 3}], 2, "vehicle", [{"type": "vehicle", "id": 3}, + {"type": "vehicle", "id": 2}], id="flat list"), + pytest.param([ + { + "type": "group", + "label": "Wichtige Fahrzeuge", + "children": [ + {"type": "vehicle", "id": 0, }, + {"type": "vehicle", "id": 1, }, + ] + }, + {"type": "vehicle", "id": 2}, + ], 4, "vehicle", [ + { + "type": "group", + "label": "Wichtige Fahrzeuge", + "children": [ + {"type": "vehicle", "id": 0, }, + {"type": "vehicle", "id": 1, }, + ] + }, + {"type": "vehicle", "id": 2}, + {"type": "vehicle", "id": 4}, + ], id="nested list"), + ] +) +def test_add_item(loadmanagement_prios: List[Dict], id: int, type: str, expected_loadmanagement_prios: List[Dict]): + # setup + c = CounterAll() + c.data.get.loadmanagement_prios = loadmanagement_prios + + # execution + c.add_loadmanagement_prio_item(type, id) + + # assert + assert c.data.get.loadmanagement_prios == expected_loadmanagement_prios + + +@pytest.mark.parametrize( + "loadmanagement_prios, id, type, expected_loadmanagement_prios", + [ + pytest.param([{"type": "vehicle", "id": 3}, {"type": "vehicle", "id": 2}], + 2, "vehicle", [{"type": "vehicle", "id": 3}], id="flat list"), + pytest.param([ + { + "type": "group", + "label": "Wichtige Fahrzeuge", + "children": [ + {"type": "vehicle", "id": 0, }, + {"type": "vehicle", "id": 1, }, + ] + }, + {"type": "vehicle", "id": 2}, + ], 2, "vehicle", [ + { + "type": "group", + "label": "Wichtige Fahrzeuge", + "children": [ + {"type": "vehicle", "id": 0, }, + {"type": "vehicle", "id": 1, }, + ] + }, + ], id="nested list"), + pytest.param([ + { + "type": "group", + "label": "Wichtige Fahrzeuge", + "children": [ + {"type": "vehicle", "id": 0, }, + {"type": "vehicle", "id": 1, }, + ] + }, + {"type": "vehicle", "id": 2}, + ], 0, "vehicle", [ + { + "type": "group", + "label": "Wichtige Fahrzeuge", + "children": [ + {"type": "vehicle", "id": 1, }, + ] + }, + {"type": "vehicle", "id": 2}, + ], id="nested list, remove from group"), + pytest.param([ + { + "type": "group", + "label": "Wichtige Fahrzeuge", + "children": [ + {"type": "vehicle", "id": 0, }, + ] + }, + {"type": "vehicle", "id": 2}, + ], 0, "vehicle", [{"type": "vehicle", "id": 2}], id="nested list, empty group"), + ] +) +def test_remove_loadmanagement_prio_item(loadmanagement_prios: List[Dict], + id: int, + type: str, + expected_loadmanagement_prios: List[Dict]): + # setup + c = CounterAll() + c.data.get.loadmanagement_prios = loadmanagement_prios + + # execution + c.remove_loadmanagement_prio_item(id) + + # assert + assert c.data.get.loadmanagement_prios == expected_loadmanagement_prios + + +def test_sort_cps_by_loadmanagement_prios_nested_same_vehicle(cp1, cp2, cp3): + """Alle LP haben das gleiche Fahrzeug zugeordnet""" + # setup + cp1.data.config.ev = 0 + cp2.data.config.ev = 0 + cp3.data.config.ev = 0 + + c = CounterAll() + c.data.get.loadmanagement_prios = [{"type": "vehicle", "id": 2}, {"type": "vehicle", "id": 0}] + + # execution + result = c.sort_cps_by_loadmanagement_prios_nested([cp1, cp2, cp3]) + + # assert - eine Gruppe mit allen CPs (sortiert nach required_current) + assert len(result) == 1 + assert result[0] == [cp3, cp2, cp1] # direkte Objektvergleiche! + + +def test_sort_cps_by_loadmanagement_prios_nested_different_vehicles(cp1, cp2, cp3): + """Alle LP haben unterschiedliche Fahrzeuge""" + # setup + cp1.data.config.ev = 1 + cp2.data.config.ev = 2 + cp3.data.config.ev = 3 + + c = CounterAll() + c.data.get.loadmanagement_prios = [ + {"type": "vehicle", "id": 3}, + {"type": "vehicle", "id": 1}, + {"type": "vehicle", "id": 2} + ] + + # execution + result = c.sort_cps_by_loadmanagement_prios_nested([cp1, cp2, cp3]) + + # assert - drei separate Gruppen + assert len(result) == 3 + assert result[0] == [cp3] # vehicle id=3 + assert result[1] == [cp1] # vehicle id=1 + assert result[2] == [cp2] # vehicle id=2 + + +def test_sort_cps_by_loadmanagement_prios_nested_with_group(cp1, cp2, cp3): + """LP mit unterschiedlichen Fahrzeugen, einige in Gruppe""" + # setup + cp1.data.config.ev = 1 + cp2.data.config.ev = 2 + cp3.data.config.ev = 3 + + c = CounterAll() + c.data.get.loadmanagement_prios = [ + {"type": "vehicle", "id": 3}, + { + "type": "group", + "label": "Wichtige Fahrzeuge", + "children": [ + {"type": "vehicle", "id": 1}, + {"type": "vehicle", "id": 2} + ] + } + ] + + # execution + result = c.sort_cps_by_loadmanagement_prios_nested([cp1, cp2, cp3]) + + # assert - zwei Gruppen + assert len(result) == 2 + assert result[0] == [cp3] # vehicle id=3 einzeln + assert result[1] == [cp1, cp2] # vehicles id=1,2 in der Gruppe + + +def test_generator_cps_by_loadmanagement_prios(cp1, cp2, cp3, monkeypatch): + # setup + mock_sort_cps = Mock(return_value=[[cp3], [cp1, cp2]]) + monkeypatch.setattr(CounterAll, "sort_cps_by_loadmanagement_prios_nested", mock_sort_cps) + + c = CounterAll() + + # execution + result = list(c.generator_cps_by_loadmanagement_prios([cp1, cp2, cp3])) + + # assert + assert len(result) == 3 + assert result[0] == (cp3, [cp3]) + assert result[1] == (cp1, [cp1, cp2]) + assert result[2] == (cp2, [cp2]) + + +def test_generator_cps_by_loadmanagement_prios_flat(cp1, cp2, cp3, monkeypatch): + # setup + mock_sort_cps = Mock(return_value=[[cp3], [cp1], [cp2]]) + monkeypatch.setattr(CounterAll, "sort_cps_by_loadmanagement_prios_nested", mock_sort_cps) + + c = CounterAll() + + # execution + result = list(c.generator_cps_by_loadmanagement_prios([cp1, cp2, cp3])) + + # assert + assert len(result) == 3 + assert result[0] == (cp3, [cp3]) + assert result[1] == (cp1, [cp1]) + assert result[2] == (cp2, [cp2]) + + +def test_generator_cps_by_loadmanagement_prios_one_group(cp1, cp2, cp3, monkeypatch): + # setup + mock_sort_cps = Mock(return_value=[[cp3, cp1, cp2]]) + monkeypatch.setattr(CounterAll, "sort_cps_by_loadmanagement_prios_nested", mock_sort_cps) + + c = CounterAll() + + # execution + result = list(c.generator_cps_by_loadmanagement_prios([cp1, cp2, cp3])) + + # assert + assert len(result) == 3 + assert result[0] == (cp3, [cp3, cp1, cp2]) + assert result[1] == (cp1, [cp1, cp2]) + assert result[2] == (cp2, [cp2]) diff --git a/packages/control/data.py b/packages/control/data.py index 1d7c779d01..dfa48c416a 100644 --- a/packages/control/data.py +++ b/packages/control/data.py @@ -19,7 +19,7 @@ from helpermodules.graph import Graph from helpermodules.subdata import SubData from control.counter import Counter -from control.counter_all import CounterAll +from control.counter_all.counter_all import CounterAll from control.ev.charge_template import ChargeTemplate from control.ev.ev import Ev from control.ev.ev_template import EvTemplate diff --git a/packages/helpermodules/command.py b/packages/helpermodules/command.py index f03fd86884..a768e58e0d 100644 --- a/packages/helpermodules/command.py +++ b/packages/helpermodules/command.py @@ -17,6 +17,7 @@ from control.chargepoint import chargepoint from control.chargepoint.chargepoint_template import get_chargepoint_template_default +from control.counter_all import counter_all from control.ev.charge_template import ChargeTemplate, get_new_charge_template from control.ev.ev_template import EvTemplateData from helpermodules import pub @@ -38,7 +39,7 @@ from helpermodules.pub import Pub, pub_single from helpermodules.subdata import SubData from helpermodules.utils.topic_parser import decode_payload, get_index -from control import bat, bridge, data, counter, counter_all, pv +from control import bat, bridge, data, counter, pv from control.ev import ev from modules.chargepoints.internal_openwb.chargepoint_module import ChargepointModule from modules.chargepoints.internal_openwb.config import InternalChargepointMode @@ -772,6 +773,9 @@ def addVehicle(self, connection_id: str, payload: dict) -> None: # add ACL roles for vehicle access, if user management is active if SubData.system_data["system"].data["security"]["user_management_active"]: add_acl_role("vehicle--access", new_id) + data.data.counter_all_data.add_loadmanagement_prio_item("vehicle", new_id) + Pub().pub("openWB/set/counter/get/loadmanagement_prios", + data.data.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: @@ -787,6 +791,9 @@ def removeVehicle(self, connection_id: str, payload: dict) -> None: if SubData.system_data["system"].data["security"]["user_management_active"]: remove_acl_role("vehicle--access", payload["data"]["id"]) remove_acl_role("vehicle--write-access", payload["data"]["id"]) + data.data.counter_all_data.remove_loadmanagement_prio_item(payload["data"]["id"]) + Pub().pub("openWB/set/counter/get/loadmanagement_prios", + data.data.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 6df640429a..e5dc648193 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -938,7 +938,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/subdata.py b/packages/helpermodules/subdata.py index 38b26d5490..ba59c3cd7f 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -9,12 +9,13 @@ import subprocess import paho.mqtt.client as mqtt -from control import bat_all, bat, counter, counter_all, general, io_device, optional, pv, pv_all +from control import bat_all, bat, counter, general, io_device, optional, pv, pv_all from control.chargepoint import chargepoint from control.chargepoint.chargepoint_all import AllChargepoints from control.chargepoint.chargepoint_data import Log from control.chargepoint.chargepoint_state_update import ChargepointStateUpdate from control.chargepoint.chargepoint_template import CpTemplate, CpTemplateData +from control.counter_all import counter_all from control.ev.charge_template import ChargeTemplate, ChargeTemplateData from control.ev import ev from control.ev.ev_template import EvTemplate, EvTemplateData diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 3384fdd9ef..301b82e15c 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -33,7 +33,7 @@ from helpermodules.utils.json_file_handler import write_and_check from helpermodules.utils.run_command import run_command from helpermodules.utils.topic_parser import decode_payload, get_index, get_second_index -from control import counter_all +from control.counter_all import counter_all, counter_all_data from control.bat_all import BatConsiderationMode from control.chargepoint.charging_type import ChargingType from control.counter import get_counter_default_config @@ -187,6 +187,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$", @@ -569,8 +570,9 @@ class UpdateConfig: ("openWB/chargepoint/get/power", 0), ("openWB/chargepoint/template/0", get_chargepoint_template_default()), ("openWB/counter/get/hierarchy", []), - ("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/counter/get/loadmanagement_prios", [{"type": "vehicle", "id": 0}]), + ("openWB/counter/config/consider_less_charging", counter_all_data.Config().consider_less_charging), + ("openWB/counter/config/home_consumption_source_id", counter_all_data.Config().home_consumption_source_id), ("openWB/vehicle/0/name", "Standard-Fahrzeug"), ("openWB/vehicle/0/info", {"manufacturer": None, "model": None}), ("openWB/vehicle/0/charge_template", ev.Ev(0).charge_template.data.id), diff --git a/packages/modules/common/store/_counter_test.py b/packages/modules/common/store/_counter_test.py index b9c8c292d8..ba178d1f2f 100644 --- a/packages/modules/common/store/_counter_test.py +++ b/packages/modules/common/store/_counter_test.py @@ -9,7 +9,7 @@ from control import data from control.chargepoint.chargepoint import Chargepoint from control.counter import Counter, CounterData, Get -from control.counter_all import CounterAll +from control.counter_all.counter_all import CounterAll from modules.chargepoints.mqtt.chargepoint_module import ChargepointModule from modules.common.component_state import BatState, ChargepointState, CounterState, InverterState from modules.common.simcount._simcounter import SimCounter diff --git a/packages/modules/common/store/_inverter_test.py b/packages/modules/common/store/_inverter_test.py index 46b2c58092..682c9ca2d8 100644 --- a/packages/modules/common/store/_inverter_test.py +++ b/packages/modules/common/store/_inverter_test.py @@ -1,6 +1,6 @@ from modules.common.store._inverter import PurgeInverterState from modules.common.component_state import InverterState -from control.counter_all import CounterAll +from control.counter_all.counter_all import CounterAll from control.bat import Bat, BatData, Get from typing import List, NamedTuple from unittest.mock import Mock diff --git a/packages/modules/devices/generic/virtual/counter_test.py b/packages/modules/devices/generic/virtual/counter_test.py index a19c60436a..645d1da104 100644 --- a/packages/modules/devices/generic/virtual/counter_test.py +++ b/packages/modules/devices/generic/virtual/counter_test.py @@ -6,7 +6,7 @@ from control import data from control.chargepoint.chargepoint import Chargepoint -from control.counter_all import CounterAll +from control.counter_all.counter_all import CounterAll from modules.chargepoints.mqtt.chargepoint_module import ChargepointModule from modules.chargepoints.mqtt.config import Mqtt from modules.common.component_state import BatState, ChargepointState, CounterState, InverterState