Skip to content
2 changes: 2 additions & 0 deletions packages/control/algorithm/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def set_current_counterdiff(diff_current: float,
diffs,
voltages_mean(chargepoint.data.get.voltages))
data.data.io_actions.dimming_set_import_power_left({"type": "cp", "id": chargepoint.num}, sum(diffs)*230)
data.data.io_actions.set_limit_loadmanager({"type": "cp", "id": chargepoint.num}, sum(diffs)*230)

chargepoint.data.set.current = current
log.info(f"LP{chargepoint.num}: Stromstärke {current}A")
Expand Down Expand Up @@ -156,6 +157,7 @@ def update_raw_data(preferenced_chargepoints: List[Chargepoint],
else:
data.data.counter_data[counter].update_values_left(diffs, voltages_mean(chargepoint.data.get.voltages))
data.data.io_actions.dimming_set_import_power_left({"type": "cp", "id": chargepoint.num}, sum(diffs)*230)
data.data.io_actions.set_limit_loadmanager({"type": "cp", "id": chargepoint.num}, sum(diffs)*230)


def consider_less_charging_chargepoint_in_loadmanagement(cp: Chargepoint, set_current: float) -> bool:
Expand Down
22 changes: 21 additions & 1 deletion packages/control/io_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
from helpermodules.constants import NO_ERROR
from modules.io_actions.controllable_consumers.dimming.api_eebus import DimmingEebus
from modules.io_actions.controllable_consumers.dimming.api_io import DimmingIo

from modules.io_actions.controllable_consumers.load_manager.api import LoadManager

from modules.io_actions.controllable_consumers.dimming_direct_control.api import DimmingDirectControl
from modules.io_actions.controllable_consumers.ripple_control_receiver.api import RippleControlReceiver
from modules.io_actions.generator_systems.stepwise_control.api_eebus import StepwiseControlEebus
Expand Down Expand Up @@ -55,7 +58,8 @@ def __init__(self, num: Union[int, str]):
class IoActions:
def __init__(self):
self.actions: Dict[int, Union[DimmingIo, DimmingEebus, DimmingDirectControl,
RippleControlReceiver, StepwiseControlEebus, StepwiseControlIo]] = {}
RippleControlReceiver, StepwiseControlEebus, StepwiseControlIo,
LoadManager]] = {}

def setup(self):
for action in self.actions.values():
Expand Down Expand Up @@ -102,3 +106,19 @@ def stepwise_control(self, device_id: int) -> Tuple[Optional[float], Loadmanagem
return action.control_stepwise()
else:
return None, LoadmanagementLimit(None, None)

def get_limit_loadmanager(self, device: Dict) -> Tuple[Optional[float], Optional[float], LoadmanagementLimit]:
for action in self.actions.values():
if isinstance(action, LoadManager):
for d in action.config.configuration.devices:
if d == device:
return action.loadmanager_get_import_power_left()
else:
return None, None, LoadmanagementLimit(None, None)

def set_limit_loadmanager(self, device: Dict, used_power: float) -> Optional[float]:
for action in self.actions.values():
if isinstance(action, LoadManager):
for d in action.config.configuration.devices:
if d == device:
return action.loadmanager_set_import_power_left(used_power, used_power/230)
3 changes: 3 additions & 0 deletions packages/control/limiting_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ class LimitingValue(Enum):
CURRENT = ", da der Maximal-Strom an Zähler {} erreicht ist."
POWER = ", da die maximale Leistung an Zähler {} erreicht ist."
UNBALANCED_LOAD = ", da die maximale Schieflast an Zähler {} erreicht ist."
LOADMANAGER = ", da der Lastmanager die Ladeleistung begrenzt."
LOADMANAGER_ERROR = (", da aufgrund eines Fehlers im Lastmanager die Ladeleistung auf die maximale Leistung "
"im Fehlerfall begrenzt wird.")
DIMMING = ", da die Dimmung die Ladeleistung begrenzt."
DIMMING_VIA_DIRECT_CONTROL = ", da die Dimmung per Direkt-Steuerung die Ladeleistung auf 4,2 kW begrenzt."
RIPPLE_CONTROL_RECEIVER = (", da der Ladepunkt durch den RSE-Kontakt auf {}% der konfigurierten Anschlussleistung "
Expand Down
30 changes: 30 additions & 0 deletions packages/control/loadmanagement.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ def get_available_currents(self,
available_currents, new_limit = self._limit_by_dimming(available_currents, cp)
limit = new_limit if new_limit.limiting_value is not None else limit

available_currents, new_limit = self._limit_loadmanager(available_currents, cp)
limit = new_limit if new_limit.limiting_value is not None else limit

available_currents, new_limit = self._limit_by_ripple_control_receiver(available_currents, cp)
limit = new_limit if new_limit.limiting_value is not None else limit
except ValueError as e:
Expand Down Expand Up @@ -181,3 +184,30 @@ def _limit_by_ripple_control_receiver(self,
if c > 0 else 0 for c in available_currents]
log.debug(f"Reduzierung durch RSE-Kontakt auf {value*100}%, maximal {max_current*value}A")
return available_currents, limit

def _limit_loadmanager(self,
Comment thread
LKuemmel marked this conversation as resolved.
available_currents: List[float],
cp: Chargepoint) -> Tuple[List[float], LoadmanagementLimit]:

loadmanager_power_left, loadmanager_current_left, limit = data.data.io_actions.get_limit_loadmanager({
"type": "cp", "id": cp.num})

if loadmanager_power_left is not None and loadmanager_current_left is not None:
if sum(available_currents)*voltages_mean(cp.data.get.voltages) > loadmanager_power_left:
phases = 3-available_currents.count(0)
overload_per_phase = (sum(available_currents) -
loadmanager_power_left/voltages_mean(cp.data.get.voltages))/phases
available_currents = [c - overload_per_phase if c > 0 else 0 for c in available_currents]
log.debug(
f"Reduzierung der Leistung auf {sum(available_currents)*voltages_mean(cp.data.get.voltages)}W "
f"durch den Lastmanager. (Leistungsgrenze: {loadmanager_power_left}W)")
return available_currents, limit
if sum(available_currents) > loadmanager_current_left:
phases = 3 - available_currents.count(0)
overload_per_phase = (sum(available_currents) - loadmanager_current_left) / phases
available_currents = [max(c - overload_per_phase, 0) if c > 0 else 0 for c in available_currents]
log.debug(
f"Reduzierung der Ströme auf {available_currents}A durch den Lastmanager. "
f"(Stromgrenze: {loadmanager_current_left}A)")
return available_currents, limit
return available_currents, limit
Comment on lines +188 to +213
18 changes: 18 additions & 0 deletions packages/helpermodules/setdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,8 @@ def process_mqtt_topic(self, msg: mqtt.MQTTMessage):
self.process_pv_topic(msg)
elif "openWB/set/mqtt/vehicle/" in msg.topic:
self.process_vehicle_topic(msg)
elif "openWB/set/mqtt/loadmanager/" in msg.topic:
self.loadmanager_topic(msg)

def process_optional_topic(self, msg: mqtt.MQTTMessage):
""" Handler für die Optionalen-Topics
Expand Down Expand Up @@ -1214,3 +1216,19 @@ def process_legacy_smart_home_topic(self, msg):

def _get_ramdisk_path(self) -> Path:
return Path(__file__).resolve().parents[2]/"ramdisk"

def loadmanager_topic(self, msg: mqtt.MQTTMessage):
""" Handler für die LoadManager-Topics

Parameters
----------
msg:
enthält Topic und Payload
"""
try:
if re.search("^openWB/set/mqtt/loadmanager/[0-9]+/set/loadmanager$", msg.topic) is not None:
self._validate_value(msg, "json")
else:
self.__unknown_topic(msg)
except Exception:
log.exception(f"Fehler im setdata-Modul: Topic {msg.topic}, Value: {msg.payload}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import logging
from typing import Optional, Tuple
from control import data
from control.limiting_value import LimitingValue, LoadmanagementLimit
from modules.common.abstract_io import AbstractIoAction
from modules.io_actions.common import check_fault_state_io_device
from modules.io_actions.controllable_consumers.load_manager.config import LoadManagerSetup
from modules.io_devices.load_manager.config import AnalogInputMapping

from modules.common.abstract_device import DeviceDescriptor

log = logging.getLogger(__name__)


class LoadManager(AbstractIoAction):
def __init__(self, config: LoadManagerSetup):
self.config = config
self.import_power_left = None
self.import_current_left = None
super().__init__()

def setup(self) -> None:
if check_fault_state_io_device(self.config.configuration.io_device):
log.warning("Fehler des IO-Geräts: Lastmanager aktiviert für Failsafe-Modus.")
max_power = self.config.configuration.max_power_on_failure
max_current = self.config.configuration.max_current_on_failure
else:
max_power = data.data.io_states[f"io_states{self.config.configuration.io_device}"
].data.get.analog_input[AnalogInputMapping.MAX_POWER.name]
max_current = sum(data.data.io_states[f"io_states{self.config.configuration.io_device}"
].data.get.analog_input[AnalogInputMapping.MAX_CURRENT.name])
self.import_power_left = max_power
self.import_current_left = max_current

def loadmanager_get_import_power_left(self) -> Tuple[Optional[float], Optional[float], LoadmanagementLimit]:
if check_fault_state_io_device(self.config.configuration.io_device):
return (self.import_power_left, self.import_current_left, LoadmanagementLimit(
LimitingValue.LOADMANAGER_ERROR.value,
LimitingValue.LOADMANAGER_ERROR))
return self.import_power_left, self.import_current_left, LoadmanagementLimit(LimitingValue.LOADMANAGER.value,
LimitingValue.LOADMANAGER)

def loadmanager_set_import_power_left(self, used_power: float, used_current: float) -> None:
self.import_power_left -= used_power
self.import_current_left -= used_current
log.debug(
f"verbleibende Dimm-Leistung: {self.import_power_left}W, verbleibender Strom: {self.import_current_left}A")


def create_action(config: LoadManagerSetup, parent_device_type: str):
return LoadManager(config=config)


device_descriptor = DeviceDescriptor(configuration_factory=LoadManagerSetup)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from dataclass_utils.factories import empty_io_pattern_boolean_factory, empty_list_factory
from modules.io_actions.groups import ActionGroup


@dataclass
class LoadManagerConfig:
io_device: Optional[int] = None
input_pattern: List[Dict] = field(default_factory=empty_io_pattern_boolean_factory)
devices: List[Dict] = field(default_factory=empty_list_factory)
# [{"type": "cp", "id": 0},
# {"type": "io", "id": 1, "digital_output": "SofortLa"}]
max_import_power: int = 0
max_power_on_failure: float = 0
max_current_on_failure: float = 0


class LoadManagerSetup:
def __init__(self,
name: str = "Begrenzung per Lastmanager",
type: str = "load_manager",
id: int = 0,
configuration: LoadManagerConfig = None):
self.name = name
self.id = id
self.configuration = configuration or LoadManagerConfig()
self.type = type
self.group = ActionGroup.CONTROLLABLE_CONSUMERS.value
Empty file.
62 changes: 62 additions & 0 deletions packages/modules/io_devices/load_manager/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env python3

import logging

from helpermodules import timecheck
from helpermodules.broker import BrokerClient
from helpermodules.utils.topic_parser import decode_payload
from modules.common.abstract_device import DeviceDescriptor
from modules.common.component_state import IoState
from modules.common.configurable_io import ConfigurableIo
from modules.io_devices.load_manager.config import AnalogInputMapping
from modules.io_devices.load_manager.config import LoadManager

log = logging.getLogger(__name__)
control_command_log = logging.getLogger("steuve_control_command")


def create_io(config: LoadManager):
received_topics = {}
broker = None

def read():
broker.start_finite_loop()
log.debug(f"Empfange MQTT Daten für Lastmanager {config.id}: {received_topics}")
io_state = IoState()
io_state.analog_input = getattr(io_state, "analog_input", None) or {}
io_state.analog_output = getattr(io_state, "analog_output", None) or {}
io_state.digital_input = getattr(io_state, "digital_input", None) or {}
io_state.digital_output = getattr(io_state, "digital_output", None) or {}

if received_topics.get(f"openWB/mqtt/loadmanager/{config.id}/set/loadmanager"):
payload = received_topics[f"openWB/mqtt/loadmanager/{config.id}/set/loadmanager"]

timestamp = float(payload["timestamp"])
age_s = timecheck.create_timestamp() - timestamp
# < = deaktiviert
if age_s > 60:
raise RuntimeError(f"Lastmanager-Daten sind veraltet: age={age_s:.1f}s, timestamp={timestamp}.")

io_state.analog_input.update({AnalogInputMapping.MAX_POWER.name: payload["max_power"]})
io_state.analog_input.update({AnalogInputMapping.MAX_CURRENT.name: payload["max_current"]})
io_state.analog_input.update({AnalogInputMapping.TIMESTAMP.name: timestamp})
return io_state

def initializer():
nonlocal broker
nonlocal received_topics

def on_connect(client, userdata, flags, rc):
client.subscribe(f"openWB/mqtt/loadmanager/{config.id}/#")

def on_message(client, userdata, message):
received_topics.update({message.topic: decode_payload(message.payload)})

received_topics = {}
broker = BrokerClient("subscribeMqttLoadmanager",
on_connect, on_message)

return ConfigurableIo(config=config, component_reader=read, component_writer=lambda: None, initializer=initializer)


device_descriptor = DeviceDescriptor(configuration_factory=LoadManager)
41 changes: 41 additions & 0 deletions packages/modules/io_devices/load_manager/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from enum import Enum
from typing import Dict, Union
from modules.common.io_setup import IoDeviceSetup


class AnalogInputMapping(Enum):
MAX_POWER = "max_power"
MAX_CURRENT = "max_current"
TIMESTAMP = "timestamp"


class LoadManagerConfiguration:
def __init__(self) -> None:
pass


def init_input():
return {"analog": {pin.name: None for pin in AnalogInputMapping}}


def init_output():
return {"analog": {}}


class LoadManager(IoDeviceSetup[LoadManagerConfiguration]):
def __init__(self,
name: str = "openWB Lastmanager",
type: str = "load_manager",
id: Union[int, str] = 0,
configuration: LoadManagerConfiguration = None,
input: Dict[str, Dict[int, float]] = None,
output: Dict[str, Dict[int, float]] = None) -> None:
self.name = name
self.type = type
self.id = id
self.configuration = configuration or LoadManagerConfiguration()
if input is None:
input = init_input()
if output is None:
output = init_output()
super().__init__(name, type, id, configuration or LoadManagerConfiguration(), input=input, output=output)