Skip to content
Draft
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
34 changes: 23 additions & 11 deletions data/config/mosquitto/public/default-dynamic-security.json
Original file line number Diff line number Diff line change
Expand Up @@ -1498,61 +1498,61 @@
},
{
"acltype": "publishClientSend",
"topic": "openWB/set/counter/config/home_consumption_source_id",
"topic": "openWB/set/counter/config/consider_less_charging",
"priority": 0,
"allow": true
},
{
"acltype": "publishClientSend",
"topic": "openWB/set/counter/config/consider_less_charging",
"topic": "openWB/set/counter/get/hierarchy",
"priority": 0,
"allow": true
},
{
"acltype": "publishClientSend",
"topic": "openWB/set/counter/get/hierarchy",
"topic": "openWB/set/counter/+/config/max_power_errorcase",
"priority": 0,
"allow": true
},
{
"acltype": "publishClientSend",
"topic": "openWB/set/counter/+/config/max_power_errorcase",
"topic": "openWB/set/counter/+/config/max_currents",
"priority": 0,
"allow": true
},
{
"acltype": "publishClientSend",
"topic": "openWB/set/counter/+/config/max_currents",
"topic": "openWB/set/counter/+/config/is_home_consumption_counter",
"priority": 0,
"allow": true
},
{
"acltype": "publishClientSend",
"topic": "openWB/set/counter/+/config/max_total_power",
"topic": "openWB/set/counter/+/config/is_home_consumption_counter_auto",
"priority": 0,
"allow": true
},
{
"acltype": "publishClientSend",
"topic": "openWB/set/pv/+/config/max_ac_out",
"topic": "openWB/set/counter/+/config/max_total_power",
"priority": 0,
"allow": true
},
{
"acltype": "publishClientReceive",
"topic": "openWB/system/security/access/LoadManagementConfiguration",
"acltype": "publishClientSend",
"topic": "openWB/set/pv/+/config/max_ac_out",
"priority": 0,
"allow": true
},
{
"acltype": "publishClientReceive",
"topic": "openWB/bat/+/config/max_power",
"topic": "openWB/system/security/access/LoadManagementConfiguration",
"priority": 0,
"allow": true
},
{
"acltype": "publishClientReceive",
"topic": "openWB/counter/config/home_consumption_source_id",
"topic": "openWB/bat/+/config/max_power",
"priority": 0,
"allow": true
},
Expand Down Expand Up @@ -1586,6 +1586,18 @@
"priority": 0,
"allow": true
},
{
"acltype": "publishClientReceive",
"topic": "openWB/counter/+/config/is_home_consumption_counter",
"priority": 0,
"allow": true
},
{
"acltype": "publishClientReceive",
"topic": "openWB/counter/+/config/is_home_consumption_counter_auto",
"priority": 0,
"allow": true
},
{
"acltype": "publishClientReceive",
"topic": "openWB/counter/+/config/max_total_power",
Expand Down
6 changes: 4 additions & 2 deletions packages/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,13 @@ def data_() -> None:
fault_state=0), config=Mock(spec=PvConfig, max_ac_out=10000)))})
data.data.counter_data.update({
"counter0": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock(
spec=CounterGet, currents=[40]*3, power=6200, daily_imported=45000, daily_exported=3000, fault_state=0))),
spec=CounterGet, currents=[40]*3, power=6200, daily_imported=45000, daily_exported=3000, fault_state=0),
config=Mock(spec=CounterConfig, is_home_consumption_counter=True, is_home_consumption_counter_auto=False))),
"counter6": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock(
spec=CounterGet, currents=[25, 10, 25], power=13800, daily_imported=20000, daily_exported=0,
imported=14000, exported=18000, fault_state=0),
config=Mock(spec=CounterConfig, max_currents=[32]*3),
config=Mock(spec=CounterConfig, max_currents=[32]*3,
is_home_consumption_counter=False, is_home_consumption_counter_auto=False),
set=Mock(spec=CounterSet, raw_currents_left=[31]*3)))})


Expand Down
11 changes: 8 additions & 3 deletions packages/control/counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def get_counter_default_config():
return {"max_power_errorcase": 7000,
"max_currents": [35]*3,
"max_total_power": 24000,
"is_home_consumption_counter": False,
"is_home_consumption_counter_auto": True
}


Expand All @@ -38,10 +40,13 @@ class ControlRangeState(Enum):

@dataclass
class Config:
max_power_errorcase: float = field(default=7000, metadata={"topic": "get/max_power_errorcase"})
max_power_errorcase: float = field(default=7000, metadata={"topic": "config/max_power_errorcase"})
max_currents: List[float] = field(default_factory=currents_list_factory, metadata={
"topic": "get/max_currents"})
max_total_power: float = field(default=0, metadata={"topic": "get/max_total_power"})
"topic": "config/max_currents"})
max_total_power: float = field(default=0, metadata={"topic": "config/max_total_power"})
is_home_consumption_counter: bool = field(default=False, metadata={"topic": "config/is_home_consumption_counter"})
is_home_consumption_counter_auto: bool = field(
default=True, metadata={"topic": "config/is_home_consumption_counter_auto"})


def config_factory() -> Config:
Expand Down
157 changes: 109 additions & 48 deletions packages/control/counter_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
from dataclasses import dataclass, field
import logging
import re
from typing import Callable, Dict, List, Optional, Tuple, Union
from typing import Any, 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
Expand All @@ -20,8 +19,6 @@

@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"})

Expand Down Expand Up @@ -101,15 +98,11 @@ def get_id_evu_counter(self) -> int:

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_source = self.get_evu_counter_str()
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
Expand All @@ -130,49 +123,91 @@ def set_home_consumption(self) -> None:
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 _get_component(self, element: Dict) -> Any:
if element["type"] == ComponentType.COUNTER.value:
return data.data.counter_data[f"counter{element['id']}"]
elif element["type"] == ComponentType.CHARGEPOINT.value:
return data.data.cp_data[f"cp{element['id']}"]
elif element["type"] == ComponentType.BAT.value:
return data.data.bat_data[f"bat{element['id']}"]
elif element["type"] == ComponentType.INVERTER.value:
return data.data.pv_data[f"pv{element['id']}"]
else:
raise ValueError(f"Unbekannter Komponententyp: {element['type']}")

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()
def _get_is_home_consumption(self, counter: Counter, parent_home_consumption: bool) -> bool:
# Wenn auto ausgeählt ist, wird die einstellung vom Parent übernommen
# Wenn nicht, wird die Einstellung vom Zähler selbst genommen
if counter.data.config.is_home_consumption_counter_auto:
return parent_home_consumption
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
return counter.data.config.is_home_consumption_counter

def _get_local_power_from_counter(self, element: Dict) -> float:
# Wird nur von Countern aufgerufen
# Gib den lokalen Verbrauch des Zählers zurück
# Bewertet noch nicht, ob Hausverbrauch oder nicht
local_power = data.data.counter_data[f"counter{element['id']}"].data.get.power

for child in element["children"]:
comp = self._get_component(child)

if comp.data.get.fault_state < 2:
local_power -= comp.data.get.power
else:
log.warning(
f"Komponente {element['type']}{comp.num} ist im Fehlerzustand und wird nicht berücksichtigt.")

return local_power

def _calc_home_consumption_from_counter(
self, element: Dict, parent_home_consumption: bool) -> float:
# Wird nur von Countern aufgerufen
# Bewertet, ob Hausverbrauch oder nicht
# Gibt den Hausverbrauch des Zählers zurück

home_consumption = 0.0
local_power = self._get_local_power_from_counter(element)

counter = self._get_component(element)
child_home_consumption = self._get_is_home_consumption(counter, parent_home_consumption)

if child_home_consumption:
home_consumption += local_power

for child in element["children"]:
comp = self._get_component(child)

if comp.data.get.fault_state < 2:
if isinstance(comp, Counter):
home_consumption += self._calc_home_consumption_from_counter(child, child_home_consumption)
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
f"Komponente {element['type']}{comp.num} ist im Fehlerzustand und wird nicht berücksichtigt.")

return home_consumption

def _calc_home_consumption(self) -> Tuple[float, List]:
evu_id = self.get_id_evu_counter()

# get_elements_for_downstream_calculation berücksichtigt Hybrid-Batterien
# wo die Bat im Wechselrichter ist (als child) und nicht direkt unter einem Zähler hängt
#
# get_elements_for_downstream_calculation liefert nur die Elemente unterhalb des EVU.
# Für die Rekursion bauen wir daher ein virtuelles Root-Element für den EVU-Zähler,
# ohne die echte Hierarchie zu verändern.

elements = self.get_elements_for_downstream_calculation(evu_id)
evu_element = {"id": evu_id, "type": ComponentType.COUNTER.value, "children": elements}

home_consumption = 0.0

# Rekursion startet immer beim EVU-Zähler.
home_consumption = self._calc_home_consumption_from_counter(evu_element, False)

home_consumption -= self.data.set.smarthome_power_excluded_from_home_consumption

return home_consumption, evu_element

def _add_hybrid_bat(self, id: int) -> List:
elements = []
Expand Down Expand Up @@ -481,6 +516,32 @@ def check_and_add(type_name: ComponentType, data_structure):
"Lastmanagements gesetzt werden kann. Bitte zuerst einen EVU-Zähler hinzufügen."),
MessageType.ERROR)

def _is_home_consumption_counter_by_id(self, counter_id: int) -> bool:
counter_entry = self.get_entry_of_element(counter_id)
if not counter_entry:
raise IndexError(f"Element {counter_id} konnte nicht in der Hierarchie gefunden werden.")
if counter_entry["type"] != ComponentType.COUNTER.value:
raise ValueError(f"Element {counter_id} ist kein Zähler.")

counter_obj = data.data.counter_data[f"counter{counter_id}"]

# Explizite Einstellung hat Vorrang, nur Auto wird vom Parent geerbt.
if not counter_obj.data.config.is_home_consumption_counter_auto:
return counter_obj.data.config.is_home_consumption_counter

parent = self.get_entry_of_parent(counter_id)
if not parent or parent["type"] != ComponentType.COUNTER.value:
# Auto am Wurzel-Zähler entspricht dem bisherigen Startwert False.
return False

return self._is_home_consumption_counter_by_id(parent["id"])

def is_home_consumption_counter(self, counter_id: int) -> bool:
"""Ermittelt den effektiven Home-Consumption-Status eines Zählers.
Berücksichtigt den Auto-Parameter entlang aller übergeordneten Zähler.
"""
return self._is_home_consumption_counter_by_id(counter_id)


def get_max_id_in_hierarchy(current_entry: List, max_id: int) -> int:
for item in current_entry:
Expand Down
Loading