diff --git a/packages/modules/common/store/_consumer.py b/packages/modules/common/store/_consumer.py index e845b76acb..f04efa6ad5 100644 --- a/packages/modules/common/store/_consumer.py +++ b/packages/modules/common/store/_consumer.py @@ -1,9 +1,10 @@ from control import data from modules.common.component_state import ConsumerState +from modules.common.component_type import ComponentType from modules.common.store import ValueStore from modules.common.store._api import LoggingValueStore from modules.common.store._broker import pub_to_broker -from modules.common.utils.component_parser import get_component_obj_by_id +from modules.common.utils.component_parser import get_hierarchy_obj_by_id class ConsumerValueStoreBroker(ValueStore[ConsumerState]): @@ -43,15 +44,15 @@ def update(self) -> None: extra_meter_id = data.data.consumer_data[f"consumer{self.delegate.delegate.num}"].data.extra_meter if extra_meter_id is not None: try: - component = get_component_obj_by_id(extra_meter_id) - component_state = component.store.delegate.delegate.state + consumer = get_hierarchy_obj_by_id(extra_meter_id, ComponentType.COUNTER.value) + consumer_state = consumer.store.delegate.delegate.state self.set(ConsumerState( - power=component_state.power, - imported=component_state.imported, - exported=component_state.exported, - voltages=component_state.voltages, - currents=component_state.currents, - powers=component_state.powers, + power=consumer_state.power, + imported=consumer_state.imported, + exported=consumer_state.exported, + voltages=consumer_state.voltages, + currents=consumer_state.currents, + powers=consumer_state.powers, )) except Exception: raise Exception(f"Fehler beim Auslesen des Verbrauchszählers {extra_meter_id} " diff --git a/packages/modules/common/store/_counter.py b/packages/modules/common/store/_counter.py index 8905912fe9..90fc4e79d6 100644 --- a/packages/modules/common/store/_counter.py +++ b/packages/modules/common/store/_counter.py @@ -10,7 +10,7 @@ from modules.common.store import ValueStore from modules.common.store._api import LoggingValueStore from modules.common.store._broker import pub_to_broker -from modules.common.utils.component_parser import get_component_obj_by_id +from modules.common.utils.component_parser import get_hierarchy_obj_by_id log = logging.getLogger(__name__) @@ -87,11 +87,20 @@ def _add_values(self, element, calc_imported_exported: bool): self.exported += element.exported self.power += element.power + def _get_assigned_extra_meter_ids(self) -> set: + assigned_extra_meter_ids = set() + for consumer in data.data.consumer_data.values(): + extra_meter_id = consumer.data.extra_meter + if extra_meter_id is not None: + assigned_extra_meter_ids.add(extra_meter_id) + return assigned_extra_meter_ids + def calc_consumers(self, elements: Dict, calc_imported_exported: bool = False) -> CounterState: + assigned_extra_meter_ids = self._get_assigned_extra_meter_ids() for element in elements: try: if element["type"] == ComponentType.CHARGEPOINT.value: - chargepoint = data.data.cp_data[f"cp{element['id']}"] + chargepoint = get_hierarchy_obj_by_id(element["id"], element["type"]) chargepoint_state = chargepoint.chargepoint_module.store.delegate.state try: self.currents = list(map(add, @@ -107,8 +116,15 @@ def calc_consumers(self, elements: Dict, calc_imported_exported: bool = False) - if calc_imported_exported: self.imported += chargepoint_state.imported self.exported += chargepoint_state.exported + elif element["type"] == ComponentType.CONSUMER.value: + consumer = get_hierarchy_obj_by_id(element["id"], element["type"]) + consumer_state = consumer.module.store.delegate.delegate.state + self._add_values(consumer_state, calc_imported_exported) + elif element["type"] == ComponentType.COUNTER.value and element["id"] in assigned_extra_meter_ids: + log.debug(f"Zähler counter{element['id']} wird übersprungen, da er als separater Zähler " + "einem Verbraucher zugeordnet ist.") else: - component = get_component_obj_by_id(element['id']) + component = get_hierarchy_obj_by_id(element["id"], element["type"]) 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}") @@ -131,7 +147,7 @@ def calc_uncounted_consumption(self) -> CounterState: 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) + parent_component = get_hierarchy_obj_by_id(parent_id, ComponentType.COUNTER.value) 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: diff --git a/packages/modules/common/store/_counter_test.py b/packages/modules/common/store/_counter_test.py index c238cb0fa7..96690a4ee8 100644 --- a/packages/modules/common/store/_counter_test.py +++ b/packages/modules/common/store/_counter_test.py @@ -131,8 +131,15 @@ def test_calc_virtual(params: Params, monkeypatch): purge = PurgeCounterState(delegate=Mock(delegate=Mock(num=0)), add_child_values=True, simcounter=SimCounter(0, 0, ComponentType.COUNTER)) - mock_comp_obj = Mock(side_effect=params.mock_comp) - monkeypatch.setattr(_counter, "get_component_obj_by_id", mock_comp_obj) + original_get_hierarchy_obj_by_id = _counter.get_hierarchy_obj_by_id + mock_comp_obj_iter = iter(params.mock_comp) + + def mock_get_hierarchy_obj_by_id(component_id, component_type): + if component_type == ComponentType.CHARGEPOINT.value: + return original_get_hierarchy_obj_by_id(component_id, component_type) + return next(mock_comp_obj_iter) + + monkeypatch.setattr(_counter, "get_hierarchy_obj_by_id", mock_get_hierarchy_obj_by_id) # execution state = purge.calc_virtual(CounterState(power=-5001, currents=[7.25]*3, exported=200, imported=100)) @@ -228,14 +235,16 @@ def test_calc_uncounted_consumption(monkeypatch): ) ) - def mock_get_component_obj_by_id(component_id): + original_get_hierarchy_obj_by_id = _counter.get_hierarchy_obj_by_id + + def mock_get_hierarchy_obj_by_id(component_id, component_type): if component_id == 0: # Parent counter return parent_counter_component elif component_id == 2: # Regular counter return regular_counter_component - return None + return original_get_hierarchy_obj_by_id(component_id, component_type) - monkeypatch.setattr(_counter, "get_component_obj_by_id", mock_get_component_obj_by_id) + monkeypatch.setattr(_counter, "get_hierarchy_obj_by_id", mock_get_hierarchy_obj_by_id) virtual_counter_purge = PurgeCounterState( delegate=Mock(delegate=Mock(num=3)), @@ -261,3 +270,80 @@ def mock_get_component_obj_by_id(component_id): ) assert vars(result_state) == vars(expected_state) + + +def test_calc_consumers_skips_assigned_extra_meter_counter(monkeypatch): + # setup + data.data.counter_all_data.data.get.hierarchy = [ + { + "id": 0, + "type": "counter", + "children": [ + {"id": 1, "type": "consumer", "children": []}, + {"id": 2, "type": "counter", "children": []}, + ], + } + ] + elements = [ + {"id": 1, "type": "consumer", "children": []}, + {"id": 2, "type": "counter", "children": []}, + ] + + consumer_state = Mock( + power=1000, + currents=[1.0, 1.0, 1.0], + imported=10, + exported=0, + ) + consumer_obj = Mock( + data=Mock(extra_meter=2), + module=Mock( + store=Mock( + delegate=Mock( + delegate=Mock(state=consumer_state) + ) + ) + ) + ) + data.data.consumer_data["consumer1"] = consumer_obj + + counter_state = CounterState( + power=2000, + currents=[2.0, 2.0, 2.0], + imported=20, + exported=0, + ) + counter_component = Mock( + store=Mock( + delegate=Mock( + delegate=Mock(state=counter_state) + ) + ) + ) + + def mock_get_hierarchy_obj_by_id(component_id, component_type): + if component_id == 1 and component_type == ComponentType.CONSUMER.value: + return consumer_obj + if component_id == 2 and component_type == ComponentType.COUNTER.value: + return counter_component + raise ValueError(f"unexpected lookup: {component_type}{component_id}") + + monkeypatch.setattr(_counter, "get_hierarchy_obj_by_id", mock_get_hierarchy_obj_by_id) + + purge = PurgeCounterState(delegate=Mock(delegate=Mock(num=0)), + add_child_values=True, + simcounter=SimCounter(0, 0, ComponentType.COUNTER)) + purge.currents = [0.0, 0.0, 0.0] + purge.power = 0 + purge.imported = 0 + purge.exported = 0 + purge.incomplete_currents = False + + # execution + result_state = purge.calc_consumers(elements, calc_imported_exported=True) + + # evaluation + assert result_state.power == 1000 + assert result_state.currents == [1.0, 1.0, 1.0] + assert result_state.imported == 10 + assert result_state.exported == 0 diff --git a/packages/modules/common/utils/component_parser.py b/packages/modules/common/utils/component_parser.py index 4b3a27be95..704d86f48b 100644 --- a/packages/modules/common/utils/component_parser.py +++ b/packages/modules/common/utils/component_parser.py @@ -4,6 +4,7 @@ from control import data from modules.common.abstract_device import AbstractDevice from modules.common.abstract_io import AbstractIoDevice +from modules.common.component_type import ComponentType from modules.common.component_type import type_to_topic_mapping log = logging.getLogger(__name__) @@ -60,12 +61,19 @@ def get_finished_component_obj_by_id(id: int, not_finished_threads: List[str]) - return None -def get_component_obj_by_id(id: int) -> Optional[Any]: - for item in data.data.system_data.values(): - if isinstance(item, AbstractDevice): - for comp in item.components.values(): - if comp.component_config.id == id: - return comp +def get_hierarchy_obj_by_id(id: int, element_type: str) -> Any: + obj = None + if element_type == ComponentType.CHARGEPOINT.value: + obj = data.data.cp_data.get(f"cp{id}") + elif element_type == ComponentType.CONSUMER.value: + obj = data.data.consumer_data.get(f"consumer{id}") + elif element_type in (ComponentType.BAT.value, ComponentType.COUNTER.value, ComponentType.INVERTER.value): + for item in data.data.system_data.values(): + if isinstance(item, AbstractDevice): + for comp in item.components.values(): + if comp.component_config.id == id: + obj = comp else: - log.error(f"Element {id} konnte keinem Gerät zugeordnet werden.") - return None + raise ValueError(f"Element {id} vom Typ {element_type} konnte nicht aufgelöst werden.") + if obj is not None: + return obj diff --git a/packages/modules/devices/generic/virtual/counter_test.py b/packages/modules/devices/generic/virtual/counter_test.py index 645d1da104..b09e58756e 100644 --- a/packages/modules/devices/generic/virtual/counter_test.py +++ b/packages/modules/devices/generic/virtual/counter_test.py @@ -135,14 +135,24 @@ def test_virtual_counter(mock_pub: Mock, params): [pytest.param(mock_comp_obj_inv_bat, hierarchy_standard, id="standard"), pytest.param(mock_comp_obj_inv_bat, hierarchy_hybrid, id="hybrid"), pytest.param(mock_comp_obj_counter_inv_bat, hierarchy_nested, id="nested")]) -def test_virtual_counter_hierarchies(mock, counter_all: Callable[[], CounterAll], data_, mock_pub: Mock, monkeypatch): +def test_virtual_counter_hierarchies(mock, + counter_all: Callable[[], CounterAll], + data_, + mock_pub: Mock, + monkeypatch: pytest.MonkeyPatch): # setup virtual_counter = counter.VirtualCounter(VirtualCounterSetup( id=0, configuration=VirtualCounterConfiguration(external_consumption=0)), device_id=0) virtual_counter.initialize() data.data.counter_all_data = counter_all() mock_comp_obj = Mock(side_effect=mock) - monkeypatch.setattr(_counter, "get_component_obj_by_id", mock_comp_obj) + + def mock_get_hierarchy_obj_by_id(component_id: int, component_type: str): + if component_type == "cp": + return data.data.cp_data[f"cp{component_id}"] + return mock_comp_obj(component_id, component_type) + + monkeypatch.setattr(_counter, "get_hierarchy_obj_by_id", mock_get_hierarchy_obj_by_id) # execution virtual_counter.update()