Skip to content

Commit d7a61a7

Browse files
authored
Virtual counter: support uncounted consumers (#2995)
* draft * fix * flake8
1 parent c4129a9 commit d7a61a7

3 files changed

Lines changed: 236 additions & 47 deletions

File tree

packages/modules/common/store/_counter.py

Lines changed: 90 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
from operator import add
3-
from typing import Optional
3+
from typing import Dict, Optional
44

55
from control import data
66
from helpermodules import compatibility
@@ -71,54 +71,101 @@ def calc_virtual(self, state: CounterState) -> CounterState:
7171
if self.add_child_values:
7272
self.currents = state.currents if state.currents else [0.0]*3
7373
self.power = state.power
74+
self.imported = state.imported if state.imported else 0
75+
self.exported = state.exported if state.exported else 0
7476
self.incomplete_currents = False
75-
76-
def add_current_power(element):
77-
if hasattr(element, "currents") and element.currents is not None:
78-
if sum(element.currents) == 0 and element.power != 0:
79-
self.currents = [0, 0, 0]
80-
self.incomplete_currents = True
81-
else:
82-
self.currents = list(map(add, self.currents, element.currents))
83-
else:
84-
self.currents = [0, 0, 0]
85-
self.incomplete_currents = True
86-
self.power += element.power
87-
8877
counter_all = data.data.counter_all_data
8978
elements = counter_all.get_elements_for_downstream_calculation(self.delegate.delegate.num)
90-
for element in elements:
91-
try:
92-
if element["type"] == ComponentType.CHARGEPOINT.value:
93-
chargepoint = data.data.cp_data[f"cp{element['id']}"]
94-
chargepoint_state = chargepoint.chargepoint_module.store.delegate.state
95-
try:
96-
self.currents = list(map(add,
97-
self.currents,
98-
convert_cp_currents_to_evu_currents(
99-
chargepoint.data.config.phase_1,
100-
chargepoint_state.currents)))
101-
except KeyError:
102-
raise KeyError("Für den virtuellen Zähler muss der Anschluss der Phasen von Ladepunkt"
103-
f" {chargepoint.data.config.name} an die Phasen des EVU Zählers "
104-
"angegeben werden.")
105-
self.power += chargepoint_state.power
106-
else:
107-
component = get_component_obj_by_id(element['id'])
108-
add_current_power(component.store.delegate.delegate.state)
109-
except Exception:
110-
log.exception(f"Fehler beim Hinzufügen der Werte für Element {element}")
111-
112-
imported, exported = self.sim_counter.sim_count(self.power)
113-
if self.incomplete_currents:
114-
self.currents = None
115-
return CounterState(currents=self.currents,
116-
power=self.power,
117-
exported=exported,
118-
imported=imported)
79+
if len(elements) == 0:
80+
return self.calc_uncounted_consumption()
81+
else:
82+
return self.calc_consumers(elements)
11983
else:
12084
return state
12185

86+
def _add_values(self, element, calc_imported_exported: bool):
87+
if hasattr(element, "currents") and element.currents is not None:
88+
if sum(element.currents) == 0 and element.power != 0:
89+
self.currents = [0, 0, 0]
90+
self.incomplete_currents = True
91+
else:
92+
self.currents = list(map(add, self.currents, element.currents))
93+
else:
94+
self.currents = [0, 0, 0]
95+
self.incomplete_currents = True
96+
if calc_imported_exported:
97+
if hasattr(element, "imported") and element.imported is not None:
98+
self.imported += element.imported
99+
if hasattr(element, "exported") and element.exported is not None:
100+
self.exported += element.exported
101+
self.power += element.power
102+
103+
def calc_consumers(self, elements: Dict, calc_imported_exported: bool = False) -> CounterState:
104+
for element in elements:
105+
try:
106+
if element["type"] == ComponentType.CHARGEPOINT.value:
107+
chargepoint = data.data.cp_data[f"cp{element['id']}"]
108+
chargepoint_state = chargepoint.chargepoint_module.store.delegate.state
109+
try:
110+
self.currents = list(map(add,
111+
self.currents,
112+
convert_cp_currents_to_evu_currents(
113+
chargepoint.data.config.phase_1,
114+
chargepoint_state.currents)))
115+
except KeyError:
116+
raise KeyError("Für den virtuellen Zähler muss der Anschluss der Phasen von Ladepunkt"
117+
f" {chargepoint.data.config.name} an die Phasen des EVU Zählers "
118+
"angegeben werden.")
119+
self.power += chargepoint_state.power
120+
if calc_imported_exported:
121+
self.imported += chargepoint_state.imported
122+
self.exported += chargepoint_state.exported
123+
else:
124+
component = get_component_obj_by_id(element['id'])
125+
self._add_values(component.store.delegate.delegate.state, calc_imported_exported)
126+
except Exception:
127+
log.exception(f"Fehler beim Hinzufügen der Werte für Element {element}")
128+
129+
if calc_imported_exported is False or self.imported is None or self.exported is None:
130+
if self.imported is None and calc_imported_exported:
131+
log.debug("Mind eine Komponente liefert keinen Zählestand für den Bezug, berechne Zählerstände")
132+
if self.exported is None and calc_imported_exported:
133+
log.debug("Mind eine Komponente liefert keinen Zählestand für die Einspeisung, berechne Zählerstände")
134+
self.imported, self.exported = self.sim_counter.sim_count(self.power)
135+
if self.incomplete_currents:
136+
self.currents = None
137+
return CounterState(currents=self.currents,
138+
power=self.power,
139+
exported=self.exported,
140+
imported=self.imported)
141+
142+
def calc_uncounted_consumption(self) -> CounterState:
143+
"""Berechnet den nicht-gezählten Verbrauch für einen virtuellen Zähler.
144+
Dazu wird der Zählerstand des übergeordneten Zählers herangezogen und davon die
145+
Werte aller anderen untergeordneten Komponenten abgezogen."""
146+
parent_id = data.data.counter_all_data.get_entry_of_parent(self.delegate.delegate.num)["id"]
147+
parent_component = get_component_obj_by_id(parent_id)
148+
if "counter" not in parent_component.component_config.type:
149+
raise Exception("Die übergeordnete Komponente des virtuellen Zählers muss ein Zähler sein.")
150+
if parent_component.store.add_child_values:
151+
raise Exception("Der übergeordnete Zähler des virtuellen Zählers darf nicht "
152+
"auch ein virtueller Zähler sein.")
153+
elements = data.data.counter_all_data.get_elements_for_downstream_calculation(parent_id)
154+
# entferne den eigenen Zähler aus der Liste
155+
elements = [el for el in elements if el["id"] != self.delegate.delegate.num]
156+
self.calc_consumers(elements, calc_imported_exported=True)
157+
log.debug(f"Erfasster Verbrauch virtueller Zähler {self.delegate.delegate.num}: "
158+
f"{self.currents}A, {self.power}W, {self.exported}Wh, {self.imported}Wh")
159+
parent_counter_get = data.data.counter_data[f"counter{parent_id}"].data.get
160+
return CounterState(
161+
currents=[parent_counter_get.currents[i] - self.currents[i]
162+
for i in range(0, 3)] if self.currents is not None else None,
163+
power=parent_counter_get.power - self.power,
164+
exported=0,
165+
imported=(parent_counter_get.imported + self.exported - self.imported -
166+
parent_counter_get.exported) if self.imported is not None else None
167+
)
168+
122169

123170
def get_counter_value_store(component_num: int,
124171
add_child_values: bool = False,

packages/modules/common/store/_counter_test.py

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from modules.common.store import _counter
1717
from modules.common.store._api import LoggingValueStore
1818
from modules.common.store._battery import BatteryValueStoreBroker, PurgeBatteryState
19-
from modules.common.store._counter import PurgeCounterState
19+
from modules.common.store._counter import CounterValueStoreBroker, PurgeCounterState
2020
from modules.common.store._inverter import InverterValueStoreBroker, PurgeInverterState
2121
from modules.devices.generic.mqtt.bat import MqttBat
2222
from modules.devices.generic.mqtt.counter import MqttCounter
@@ -138,3 +138,125 @@ def test_calc_virtual(params: Params, monkeypatch):
138138

139139
# evaluation
140140
assert vars(state) == vars(params.expected_state)
141+
142+
143+
def test_calc_uncounted_consumption(monkeypatch):
144+
"""
145+
Test für calc_uncounted_consumption mit folgendem Szenario:
146+
- Übergeordnete Ebene: Ein Zähler (id=0, parent counter)
147+
- Gleiche Ebene wie virtueller Zähler: Ein Ladepunkt (id=1) und ein weiterer Zähler (id=2)
148+
- Virtueller Zähler: id=3 (soll nicht-gezählten Verbrauch berechnen)
149+
150+
Hierarchie:
151+
Counter 0 (parent, 8000W, 1kWh importiert, 0.5kWh exportiert)
152+
├── Chargepoint 1 (3000W, 150Wh importiert, 0Wh exportiert)
153+
├── Counter 2 (2000W, 300Wh importiert, 100Wh exportiert)
154+
└── Virtual Counter 3 (uncounted: 8000 - 3000 - 2000 = 3000W, 0.15kWh imp, 0kWh exp)
155+
"""
156+
# setup
157+
data.data_init(Mock())
158+
data.data.counter_all_data = CounterAll()
159+
data.data.counter_all_data.data.get.hierarchy = [
160+
{
161+
"id": 0,
162+
"type": "counter",
163+
"children": [
164+
{"id": 1, "type": "cp", "children": []},
165+
{"id": 2, "type": "counter", "children": []},
166+
{"id": 3, "type": "counter", "children": []}
167+
]
168+
}
169+
]
170+
171+
data.data.counter_data["counter0"] = Mock(
172+
spec=Counter,
173+
data=Mock(
174+
spec=CounterData,
175+
get=Mock(
176+
spec=Get,
177+
power=8000,
178+
exported=500,
179+
imported=1000,
180+
currents=[20.0, 22.0, 18.0]
181+
)
182+
)
183+
)
184+
185+
add_chargepoint(1)
186+
data.data.cp_data["cp1"].data.get.power = 3000
187+
data.data.cp_data["cp1"].data.get.currents = [8.0, 9.0, 7.0]
188+
data.data.cp_data["cp1"].chargepoint_module.store.delegate.state.power = 3000
189+
data.data.cp_data["cp1"].chargepoint_module.store.delegate.state.currents = [8.0, 9.0, 7.0]
190+
data.data.cp_data["cp1"].chargepoint_module.store.delegate.state.imported = 150
191+
data.data.cp_data["cp1"].chargepoint_module.store.delegate.state.exported = 0
192+
193+
data.data.counter_data["counter2"] = Mock(
194+
spec=Counter,
195+
data=Mock(
196+
spec=CounterData,
197+
get=Mock(
198+
spec=Get,
199+
power=2000,
200+
exported=100,
201+
imported=300,
202+
currents=[5.0, 6.0, 4.0]
203+
)
204+
)
205+
)
206+
207+
parent_counter_component = Mock()
208+
parent_counter_component.component_config.type = "counter"
209+
parent_counter_component.store.add_child_values = False
210+
211+
regular_counter_component = Mock(
212+
spec=MqttCounter,
213+
store=Mock(
214+
spec=PurgeCounterState,
215+
delegate=Mock(
216+
spec=LoggingValueStore,
217+
delegate=Mock(
218+
spec=CounterValueStoreBroker,
219+
state=CounterState(
220+
power=2000,
221+
exported=100,
222+
imported=300,
223+
currents=[5.0, 6.0, 4.0]
224+
)
225+
)
226+
)
227+
)
228+
)
229+
230+
def mock_get_component_obj_by_id(component_id):
231+
if component_id == 0: # Parent counter
232+
return parent_counter_component
233+
elif component_id == 2: # Regular counter
234+
return regular_counter_component
235+
return None
236+
237+
monkeypatch.setattr(_counter, "get_component_obj_by_id", mock_get_component_obj_by_id)
238+
239+
virtual_counter_purge = PurgeCounterState(
240+
delegate=Mock(delegate=Mock(num=3)),
241+
add_child_values=True,
242+
simcounter=SimCounter(0, 0, prefix="virtual")
243+
)
244+
245+
# execution
246+
result_state = virtual_counter_purge.calc_virtual(CounterState())
247+
248+
# evaluation
249+
# Erwartete Werte: Parent Counter - (Chargepoint + Regular Counter)
250+
# Power: 8000 - (3000 + 2000) = 3000W
251+
# 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]
252+
# Imported: 1000 - (150 + 300) = 550
253+
# Exported: 500 - (0 + 100) = 400
254+
255+
expected_state = CounterState(
256+
power=3000,
257+
currents=[7.0, 7.0, 7.0],
258+
imported=150,
259+
exported=0
260+
)
261+
262+
assert vars(result_state) == vars(expected_state)

packages/modules/loadvars.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@ def get_values(self) -> None:
2929
levels = data.data.counter_all_data.get_list_of_elements_per_level()
3030
levels.reverse()
3131
for level in levels:
32-
self._update_values_of_level(level, not_finished_threads)
32+
self._update_values_of_level_buttom_top(level, not_finished_threads)
3333
wait_for_module_update_completed(self.event_module_update_completed, topic)
3434
data.data.copy_module_data()
35+
self._update_values_virtual_counter_uncounted_consumption(not_finished_threads)
36+
wait_for_module_update_completed(self.event_module_update_completed, topic)
37+
data.data.copy_module_data()
3538
wait_for_module_update_completed(self.event_module_update_completed, topic)
3639
joined_thread_handler(self._get_io(), data.data.general_data.data.control_interval/3)
3740
joined_thread_handler(self._set_io(), data.data.general_data.data.control_interval/3)
@@ -59,8 +62,8 @@ def _set_values(self) -> List[str]:
5962
log.exception(f"Fehler im loadvars-Modul bei Element {cp.num}")
6063
return joined_thread_handler(modules_threads, data.data.general_data.data.control_interval/3)
6164

62-
def _update_values_of_level(self, elements, not_finished_threads: List[str]) -> None:
63-
"""Threads, um von der niedrigsten Ebene der Hierarchie Werte ggf. miteinander zu verrechnen und zu
65+
def _update_values_of_level_buttom_top(self, elements, not_finished_threads: List[str]) -> None:
66+
"""Threads, um von der niedrigsten Ebene der Hierarchie beginnend Werte ggf. miteinander zu verrechnen und zu
6467
veröffentlichen"""
6568
modules_threads: List[Thread] = []
6669
for element in elements:
@@ -83,6 +86,23 @@ def _update_values_of_level(self, elements, not_finished_threads: List[str]) ->
8386
log.exception(f"Fehler im loadvars-Modul bei Element {element}")
8487
joined_thread_handler(modules_threads, data.data.general_data.data.control_interval/3)
8588

89+
def _update_values_virtual_counter_uncounted_consumption(self, not_finished_threads: List[str]) -> None:
90+
modules_threads: List[Thread] = []
91+
for counter in data.data.counter_data.values():
92+
try:
93+
component = get_finished_component_obj_by_id(counter.num, not_finished_threads)
94+
if component.component_config.type == "virtual":
95+
if len(data.data.counter_all_data.get_entry_of_element(counter.num)["children"]) == 0:
96+
thread_name = f"component{component.component_config.id}"
97+
if thread_name not in not_finished_threads:
98+
modules_threads.append(Thread(
99+
target=update_values,
100+
args=(component,),
101+
name=f"component{component.component_config.id}"))
102+
except Exception:
103+
log.exception(f"Fehler im loadvars-Modul bei Zähler {counter}")
104+
joined_thread_handler(modules_threads, data.data.general_data.data.control_interval/3)
105+
86106
def _get_io(self) -> List[Thread]:
87107
threads = [] # type: List[Thread]
88108
try:

0 commit comments

Comments
 (0)