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
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(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(sum(diffs)*230)


def consider_less_charging_chargepoint_in_loadmanagement(cp: Chargepoint, set_current: float) -> bool:
Expand Down
18 changes: 17 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,15 @@ 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) -> Tuple[Optional[float], Optional[float], LoadmanagementLimit]:
for action in self.actions.values():
if isinstance(action, LoadManager):
return action.loadmanager_get_import_power_left()
else:
return None, None, LoadmanagementLimit(None, None)

def set_limit_loadmanager(self, used_power: float) -> Optional[float]:
for action in self.actions.values():
if isinstance(action, LoadManager):
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
29 changes: 29 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,29 @@ 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.
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()

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 +212
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,27 @@
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from dataclass_utils.factories import empty_io_pattern_boolean_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)

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)