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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 90 additions & 43 deletions packages/modules/common/store/_counter.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down
124 changes: 123 additions & 1 deletion packages/modules/common/store/_counter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
26 changes: 23 additions & 3 deletions packages/modules/loadvars.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down