diff --git a/packages/modules/common/store/_counter.py b/packages/modules/common/store/_counter.py index b6a359af8d..5e97045b6d 100644 --- a/packages/modules/common/store/_counter.py +++ b/packages/modules/common/store/_counter.py @@ -1,6 +1,6 @@ import logging from operator import add -from typing import Optional +from typing import Dict, Optional from control import data from helpermodules import compatibility @@ -71,54 +71,101 @@ def calc_virtual(self, state: CounterState) -> CounterState: if self.add_child_values: self.currents = state.currents if state.currents else [0.0]*3 self.power = state.power + self.imported = state.imported if state.imported else 0 + self.exported = state.exported if state.exported else 0 self.incomplete_currents = False - - def add_current_power(element): - if hasattr(element, "currents") and element.currents is not None: - if sum(element.currents) == 0 and element.power != 0: - self.currents = [0, 0, 0] - self.incomplete_currents = True - else: - self.currents = list(map(add, self.currents, element.currents)) - else: - self.currents = [0, 0, 0] - self.incomplete_currents = True - self.power += element.power - counter_all = data.data.counter_all_data elements = counter_all.get_elements_for_downstream_calculation(self.delegate.delegate.num) - for element in elements: - try: - if element["type"] == ComponentType.CHARGEPOINT.value: - chargepoint = data.data.cp_data[f"cp{element['id']}"] - chargepoint_state = chargepoint.chargepoint_module.store.delegate.state - try: - self.currents = list(map(add, - self.currents, - convert_cp_currents_to_evu_currents( - chargepoint.data.config.phase_1, - chargepoint_state.currents))) - except KeyError: - raise KeyError("Für den virtuellen Zähler muss der Anschluss der Phasen von Ladepunkt" - f" {chargepoint.data.config.name} an die Phasen des EVU Zählers " - "angegeben werden.") - self.power += chargepoint_state.power - else: - component = get_component_obj_by_id(element['id']) - add_current_power(component.store.delegate.delegate.state) - except Exception: - log.exception(f"Fehler beim Hinzufügen der Werte für Element {element}") - - imported, exported = self.sim_counter.sim_count(self.power) - if self.incomplete_currents: - self.currents = None - return CounterState(currents=self.currents, - power=self.power, - exported=exported, - imported=imported) + if len(elements) == 0: + return self.calc_uncounted_consumption() + else: + return self.calc_consumers(elements) else: return state + def _add_values(self, element, calc_imported_exported: bool): + if hasattr(element, "currents") and element.currents is not None: + if sum(element.currents) == 0 and element.power != 0: + self.currents = [0, 0, 0] + self.incomplete_currents = True + else: + self.currents = list(map(add, self.currents, element.currents)) + else: + self.currents = [0, 0, 0] + self.incomplete_currents = True + if calc_imported_exported: + if hasattr(element, "imported") and element.imported is not None: + self.imported += element.imported + if hasattr(element, "exported") and element.exported is not None: + self.exported += element.exported + self.power += element.power + + def calc_consumers(self, elements: Dict, calc_imported_exported: bool = False) -> CounterState: + for element in elements: + try: + if element["type"] == ComponentType.CHARGEPOINT.value: + chargepoint = data.data.cp_data[f"cp{element['id']}"] + chargepoint_state = chargepoint.chargepoint_module.store.delegate.state + try: + self.currents = list(map(add, + self.currents, + convert_cp_currents_to_evu_currents( + chargepoint.data.config.phase_1, + chargepoint_state.currents))) + except KeyError: + raise KeyError("Für den virtuellen Zähler muss der Anschluss der Phasen von Ladepunkt" + f" {chargepoint.data.config.name} an die Phasen des EVU Zählers " + "angegeben werden.") + self.power += chargepoint_state.power + if calc_imported_exported: + self.imported += chargepoint_state.imported + self.exported += chargepoint_state.exported + else: + component = get_component_obj_by_id(element['id']) + self._add_values(component.store.delegate.delegate.state, calc_imported_exported) + except Exception: + log.exception(f"Fehler beim Hinzufügen der Werte für Element {element}") + + if calc_imported_exported is False or self.imported is None or self.exported is None: + if self.imported is None and calc_imported_exported: + log.debug("Mind eine Komponente liefert keinen Zählestand für den Bezug, berechne Zählerstände") + if self.exported is None and calc_imported_exported: + log.debug("Mind eine Komponente liefert keinen Zählestand für die Einspeisung, berechne Zählerstände") + self.imported, self.exported = self.sim_counter.sim_count(self.power) + if self.incomplete_currents: + self.currents = None + return CounterState(currents=self.currents, + power=self.power, + exported=self.exported, + imported=self.imported) + + def calc_uncounted_consumption(self) -> CounterState: + """Berechnet den nicht-gezählten Verbrauch für einen virtuellen Zähler. + Dazu wird der Zählerstand des übergeordneten Zählers herangezogen und davon die + Werte aller anderen untergeordneten Komponenten abgezogen.""" + parent_id = data.data.counter_all_data.get_entry_of_parent(self.delegate.delegate.num)["id"] + parent_component = get_component_obj_by_id(parent_id) + if "counter" not in parent_component.component_config.type: + raise Exception("Die übergeordnete Komponente des virtuellen Zählers muss ein Zähler sein.") + if parent_component.store.add_child_values: + raise Exception("Der übergeordnete Zähler des virtuellen Zählers darf nicht " + "auch ein virtueller Zähler sein.") + elements = data.data.counter_all_data.get_elements_for_downstream_calculation(parent_id) + # entferne den eigenen Zähler aus der Liste + elements = [el for el in elements if el["id"] != self.delegate.delegate.num] + self.calc_consumers(elements, calc_imported_exported=True) + log.debug(f"Erfasster Verbrauch virtueller Zähler {self.delegate.delegate.num}: " + f"{self.currents}A, {self.power}W, {self.exported}Wh, {self.imported}Wh") + parent_counter_get = data.data.counter_data[f"counter{parent_id}"].data.get + return CounterState( + currents=[parent_counter_get.currents[i] - self.currents[i] + for i in range(0, 3)] if self.currents is not None else None, + power=parent_counter_get.power - self.power, + exported=0, + imported=(parent_counter_get.imported + self.exported - self.imported - + parent_counter_get.exported) if self.imported is not None else None + ) + def get_counter_value_store(component_num: int, add_child_values: bool = False, diff --git a/packages/modules/common/store/_counter_test.py b/packages/modules/common/store/_counter_test.py index b9e20cd649..b9c8c292d8 100644 --- a/packages/modules/common/store/_counter_test.py +++ b/packages/modules/common/store/_counter_test.py @@ -16,7 +16,7 @@ from modules.common.store import _counter from modules.common.store._api import LoggingValueStore from modules.common.store._battery import BatteryValueStoreBroker, PurgeBatteryState -from modules.common.store._counter import PurgeCounterState +from modules.common.store._counter import CounterValueStoreBroker, PurgeCounterState from modules.common.store._inverter import InverterValueStoreBroker, PurgeInverterState from modules.devices.generic.mqtt.bat import MqttBat from modules.devices.generic.mqtt.counter import MqttCounter @@ -138,3 +138,125 @@ def test_calc_virtual(params: Params, monkeypatch): # evaluation assert vars(state) == vars(params.expected_state) + + +def test_calc_uncounted_consumption(monkeypatch): + """ + Test für calc_uncounted_consumption mit folgendem Szenario: + - Übergeordnete Ebene: Ein Zähler (id=0, parent counter) + - Gleiche Ebene wie virtueller Zähler: Ein Ladepunkt (id=1) und ein weiterer Zähler (id=2) + - Virtueller Zähler: id=3 (soll nicht-gezählten Verbrauch berechnen) + + Hierarchie: + Counter 0 (parent, 8000W, 1kWh importiert, 0.5kWh exportiert) + ├── Chargepoint 1 (3000W, 150Wh importiert, 0Wh exportiert) + ├── Counter 2 (2000W, 300Wh importiert, 100Wh exportiert) + └── Virtual Counter 3 (uncounted: 8000 - 3000 - 2000 = 3000W, 0.15kWh imp, 0kWh exp) + """ + # setup + data.data_init(Mock()) + data.data.counter_all_data = CounterAll() + data.data.counter_all_data.data.get.hierarchy = [ + { + "id": 0, + "type": "counter", + "children": [ + {"id": 1, "type": "cp", "children": []}, + {"id": 2, "type": "counter", "children": []}, + {"id": 3, "type": "counter", "children": []} + ] + } + ] + + data.data.counter_data["counter0"] = Mock( + spec=Counter, + data=Mock( + spec=CounterData, + get=Mock( + spec=Get, + power=8000, + exported=500, + imported=1000, + currents=[20.0, 22.0, 18.0] + ) + ) + ) + + add_chargepoint(1) + data.data.cp_data["cp1"].data.get.power = 3000 + data.data.cp_data["cp1"].data.get.currents = [8.0, 9.0, 7.0] + data.data.cp_data["cp1"].chargepoint_module.store.delegate.state.power = 3000 + data.data.cp_data["cp1"].chargepoint_module.store.delegate.state.currents = [8.0, 9.0, 7.0] + data.data.cp_data["cp1"].chargepoint_module.store.delegate.state.imported = 150 + data.data.cp_data["cp1"].chargepoint_module.store.delegate.state.exported = 0 + + data.data.counter_data["counter2"] = Mock( + spec=Counter, + data=Mock( + spec=CounterData, + get=Mock( + spec=Get, + power=2000, + exported=100, + imported=300, + currents=[5.0, 6.0, 4.0] + ) + ) + ) + + parent_counter_component = Mock() + parent_counter_component.component_config.type = "counter" + parent_counter_component.store.add_child_values = False + + regular_counter_component = Mock( + spec=MqttCounter, + store=Mock( + spec=PurgeCounterState, + delegate=Mock( + spec=LoggingValueStore, + delegate=Mock( + spec=CounterValueStoreBroker, + state=CounterState( + power=2000, + exported=100, + imported=300, + currents=[5.0, 6.0, 4.0] + ) + ) + ) + ) + ) + + def mock_get_component_obj_by_id(component_id): + if component_id == 0: # Parent counter + return parent_counter_component + elif component_id == 2: # Regular counter + return regular_counter_component + return None + + monkeypatch.setattr(_counter, "get_component_obj_by_id", mock_get_component_obj_by_id) + + virtual_counter_purge = PurgeCounterState( + delegate=Mock(delegate=Mock(num=3)), + add_child_values=True, + simcounter=SimCounter(0, 0, prefix="virtual") + ) + + # execution + result_state = virtual_counter_purge.calc_virtual(CounterState()) + + # evaluation + # Erwartete Werte: Parent Counter - (Chargepoint + Regular Counter) + # Power: 8000 - (3000 + 2000) = 3000W + # Currents: [20.0, 22.0, 18.0] - ([8.0, 9.0, 7.0] + [5.0, 6.0, 4.0]) = [7.0, 7.0, 7.0] + # Imported: 1000 - (150 + 300) = 550 + # Exported: 500 - (0 + 100) = 400 + + expected_state = CounterState( + power=3000, + currents=[7.0, 7.0, 7.0], + imported=150, + exported=0 + ) + + assert vars(result_state) == vars(expected_state) diff --git a/packages/modules/loadvars.py b/packages/modules/loadvars.py index f0b3815052..76009ec8f2 100644 --- a/packages/modules/loadvars.py +++ b/packages/modules/loadvars.py @@ -29,9 +29,12 @@ def get_values(self) -> None: levels = data.data.counter_all_data.get_list_of_elements_per_level() levels.reverse() for level in levels: - self._update_values_of_level(level, not_finished_threads) + self._update_values_of_level_buttom_top(level, not_finished_threads) wait_for_module_update_completed(self.event_module_update_completed, topic) data.data.copy_module_data() + self._update_values_virtual_counter_uncounted_consumption(not_finished_threads) + wait_for_module_update_completed(self.event_module_update_completed, topic) + data.data.copy_module_data() wait_for_module_update_completed(self.event_module_update_completed, topic) joined_thread_handler(self._get_io(), data.data.general_data.data.control_interval/3) joined_thread_handler(self._set_io(), data.data.general_data.data.control_interval/3) @@ -59,8 +62,8 @@ def _set_values(self) -> List[str]: log.exception(f"Fehler im loadvars-Modul bei Element {cp.num}") return joined_thread_handler(modules_threads, data.data.general_data.data.control_interval/3) - def _update_values_of_level(self, elements, not_finished_threads: List[str]) -> None: - """Threads, um von der niedrigsten Ebene der Hierarchie Werte ggf. miteinander zu verrechnen und zu + def _update_values_of_level_buttom_top(self, elements, not_finished_threads: List[str]) -> None: + """Threads, um von der niedrigsten Ebene der Hierarchie beginnend Werte ggf. miteinander zu verrechnen und zu veröffentlichen""" modules_threads: List[Thread] = [] for element in elements: @@ -83,6 +86,23 @@ def _update_values_of_level(self, elements, not_finished_threads: List[str]) -> log.exception(f"Fehler im loadvars-Modul bei Element {element}") joined_thread_handler(modules_threads, data.data.general_data.data.control_interval/3) + def _update_values_virtual_counter_uncounted_consumption(self, not_finished_threads: List[str]) -> None: + modules_threads: List[Thread] = [] + for counter in data.data.counter_data.values(): + try: + component = get_finished_component_obj_by_id(counter.num, not_finished_threads) + if component.component_config.type == "virtual": + if len(data.data.counter_all_data.get_entry_of_element(counter.num)["children"]) == 0: + thread_name = f"component{component.component_config.id}" + if thread_name not in not_finished_threads: + modules_threads.append(Thread( + target=update_values, + args=(component,), + name=f"component{component.component_config.id}")) + except Exception: + log.exception(f"Fehler im loadvars-Modul bei Zähler {counter}") + joined_thread_handler(modules_threads, data.data.general_data.data.control_interval/3) + def _get_io(self) -> List[Thread]: threads = [] # type: List[Thread] try: