Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/control/bat.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def get_factory() -> Get:
@dataclass
class Set:
power_limit: Optional[int] = field(default=None, metadata={"topic": "set/power_limit"})
error_timer: Optional[float] = field(default=None, metadata={"topic": "set/error_timer"})
evu_power: Optional[int] = field(default=None, metadata={"topic": "set/evu_power"})


Expand Down
21 changes: 19 additions & 2 deletions packages/control/bat_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
from typing import List, Optional, Tuple

from control import data
from control.error_state import error_duration_exceeded, tick_error_timer
from helpermodules.constants import NO_ERROR
from modules.common.abstract_device import AbstractDevice
from modules.common.component_context import SingleComponentUpdateContext
from modules.common.utils.component_parser import get_component_name_by_id

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -150,6 +152,10 @@ def calc_power_for_all_components(self):
fault_state = 0
for battery in data.data.bat_data.values():
try:
# power ist bei andauerndem Fehler bereits vom Modul auf 0 gesetzt; error_timer
# wird hier nur noch für die aktive Speichersteuerung getickt.
battery.data.set.error_timer = tick_error_timer(
battery.data.get.fault_state, battery.data.set.error_timer)
power += battery.data.get.power
except Exception:
log.exception(f"Fehler im Bat-Modul {battery.num}")
Expand Down Expand Up @@ -225,6 +231,9 @@ def _set_bat_power_active_control(self, power):
if power is not None:
for bat_component in controllable_bat_components:
bat_component_data = data.data.bat_data[f"bat{bat_component.component_config.id}"].data
if error_duration_exceeded(bat_component_data.get.fault_state, bat_component_data.set.error_timer):
# SoC/Kapazität dieses Speichers sind nicht mehr aktuell, nicht in die Summen einrechnen.
continue
if bat_component_data.get.soc < self.data.config.bat_control_max_soc:
max_charge_power_total += bat_component_data.get.max_charge_power
bat_ready_to_charge += 1
Expand All @@ -241,8 +250,16 @@ def _set_bat_power_active_control(self, power):
# Leistung an einzelne Speicher übergeben
for bat_component in controllable_bat_components:
bat_component_data = data.data.bat_data[f"bat{bat_component.component_config.id}"].data
# Falls keine Leistung übergeben wird greift die Eigenregelung der Speicher
if power is None:
# Falls keine Leistung übergeben wird oder der Speicher seit COMPONENT_ERROR_DURATION Sekunden im
# Fehlerzustand ist, greift die Eigenregelung der Speicher.
if error_duration_exceeded(bat_component_data.get.fault_state, bat_component_data.set.error_timer):
power_limit = None
bat_component_data.get.state_str = (
f"Fehler beim Auslesen des Speichers {get_component_name_by_id(bat_component.component_config.id)}"
". Die aktive Speichersteuerung berücksichtigt diesen Speicher nicht mehr.")
data.data.bat_data[f"bat{bat_component.component_config.id}"].data.set.power_limit = power_limit
continue
elif power is None:
power_limit = None
bat_component_data.get.state_str = "Keine Steuerung"
log.debug(("Speichersteuerung: Eigenregelung - Speicher "
Expand Down
100 changes: 100 additions & 0 deletions packages/control/bat_all_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from control.chargepoint.chargepoint_all import AllChargepointData, AllChargepoints, AllGet
from control.general import ChargemodeConfigBat, General
from control.pv import Config, Get, Pv, PvData
from helpermodules import timecheck
from modules.common.fault_state_level import FaultStateLevel
from modules.devices.generic.mqtt.bat import MqttBat
from modules.devices.generic.mqtt.config import MqttBatSetup

Expand Down Expand Up @@ -62,6 +64,104 @@ def test_get_charging_power_left_diff_hybrid(bat_power: int,
assert b_all.data.set.charging_power_left == expected_power


def test_calc_power_for_all_components_trusts_already_zeroed_battery_but_ticks_error_timer_for_active_control(data_):
# setup
# bat2 ist seit über 60s im Fehlerzustand, power schon vom Modul genullt - BatAll summiert nur noch.
data.data.bat_data = {"bat1": Bat(1), "bat2": Bat(2)}
data.data.bat_data["bat1"].data.get.power = -500
data.data.bat_data["bat2"].data.get.power = 0
data.data.bat_data["bat2"].data.get.fault_state = FaultStateLevel.ERROR
b_all = BatAll()

# execution
b_all.calc_power_for_all_components()

# evaluation
assert b_all.data.get.power == -500
assert data.data.bat_data["bat2"].data.set.error_timer is not None


def test_calc_power_for_all_components_recovers_after_error_clears(data_):
# setup
# Speicher war im Fehlerzustand (error_timer noch gesetzt), liefert jetzt aber wieder gültige Werte
# (fault_state == 0) - der alte error_timer darf nicht mehr dazu führen, dass die aktive Speichersteuerung
# ihn weiterhin als fehlerhaft behandelt.
data.data.bat_data = {"bat1": Bat(1)}
data.data.bat_data["bat1"].data.get.power = -800
data.data.bat_data["bat1"].data.get.fault_state = 0
data.data.bat_data["bat1"].data.set.error_timer = timecheck.create_timestamp() - 61
b_all = BatAll()

# execution
b_all.calc_power_for_all_components()

# evaluation
assert data.data.bat_data["bat1"].data.get.power == -800
assert data.data.bat_data["bat1"].data.set.error_timer is None
assert b_all.data.get.power == -800


def test_set_bat_power_active_control_ignores_battery_after_60s(data_, monkeypatch):
# setup
# Speicher seit über 60s im Fehlerzustand - die aktive Speichersteuerung darf sich nicht mehr auf dessen
# (möglicherweise veraltete) SoC/Kapazität stützen und muss ihn auf Eigenregelung stellen.
error_timer = timecheck.create_timestamp() - 61
b_all = BatAll()
b_all.data.config.bat_control_max_soc = 95
b_all.data.config.bat_control_min_soc = 5
data.data.bat_data["bat2"].data.get.soc = 50
data.data.bat_data["bat2"].data.get.max_charge_power = 3000
data.data.bat_data["bat2"].data.get.max_discharge_power = 3000
data.data.bat_data["bat2"].data.get.fault_state = FaultStateLevel.ERROR
data.data.bat_data["bat2"].data.set.error_timer = error_timer
get_bat_components_by_controllability_mock = Mock(return_value=([MqttBat(MqttBatSetup(id=2), device_id=0)], []))
monkeypatch.setattr(bat_all, "get_bat_components_by_controllability",
get_bat_components_by_controllability_mock)
monkeypatch.setattr(bat_all, "get_component_name_by_id", Mock(return_value="Speicher 2"))

# execution
b_all._set_bat_power_active_control(1000)

# evaluation
assert data.data.bat_data["bat2"].data.set.power_limit is None
assert "Speicher 2" in data.data.bat_data["bat2"].data.get.state_str
assert data.data.bat_data["bat2"].data.set.error_timer == error_timer


def test_set_bat_power_active_control_excludes_faulted_battery_from_capacity_totals(data_, monkeypatch):
# setup
# bat2 seit über 60s im Fehlerzustand - dessen (evtl. veraltete) max_charge_power darf nicht in die
# Kapazitäts-Summe einfließen, mit der die Leistung auf die gesunden Speicher verteilt wird.
error_timer = timecheck.create_timestamp() - 61
b_all = BatAll()
b_all.data.config.bat_control_max_soc = 95
b_all.data.config.bat_control_min_soc = 5
data.data.bat_data["bat1"] = Bat(1)
data.data.bat_data["bat1"].data.get.soc = 50
data.data.bat_data["bat1"].data.get.max_charge_power = 2000
data.data.bat_data["bat1"].data.get.max_discharge_power = 2000
data.data.bat_data["bat2"].data.get.soc = 50
data.data.bat_data["bat2"].data.get.max_charge_power = 3000
data.data.bat_data["bat2"].data.get.max_discharge_power = 3000
data.data.bat_data["bat2"].data.get.fault_state = FaultStateLevel.ERROR
data.data.bat_data["bat2"].data.set.error_timer = error_timer
get_bat_components_by_controllability_mock = Mock(return_value=(
[MqttBat(MqttBatSetup(id=1), device_id=0), MqttBat(MqttBatSetup(id=2), device_id=0)], []))
monkeypatch.setattr(bat_all, "get_bat_components_by_controllability",
get_bat_components_by_controllability_mock)
monkeypatch.setattr(bat_all, "get_component_name_by_id", Mock(return_value="Speicher 2"))
get_evu_counter_mock = Mock(return_value=Mock(data=Mock(get=Mock(power=0))))
monkeypatch.setattr(data.data.counter_all_data, "get_evu_counter", get_evu_counter_mock)

# execution - 1000W laden anfordern
b_all._set_bat_power_active_control(1000)

# evaluation
# bat1 bekommt die volle angeforderte Leistung, da bat2s Kapazität nicht mit eingerechnet wird
assert data.data.bat_data["bat1"].data.set.power_limit == 1000
assert data.data.bat_data["bat2"].data.set.power_limit is None


@pytest.mark.parametrize(
"hybrid_bat_ids, non_hybrid_bat_ids, hybrid_inverter_ids, expected_power",
[
Expand Down
9 changes: 9 additions & 0 deletions packages/control/consumer/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
from control.chargemode import Chargemode
from control.chargepoint.chargepoint_state import CHARGING_STATES, ChargepointState
from control.consumer.consumer_data import ConsumerData, ConsumerUsage, WaitForStartStates
from control.error_state import error_duration_exceeded, tick_error_timer
from control.load_protocol import Load
from control.text import format_next_time_charging_start
from helpermodules import timecheck
from helpermodules.abstract_plans import ScheduledPlanConsumer
from helpermodules.phase_handling import convert_single_evu_phase_to_cp_phase, voltages_mean
from modules.common.configurable_consumer import ConfigurableConsumer
from modules.common.utils.component_parser import get_component_name_by_id

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -60,6 +62,9 @@ def update(self):
self.data.get.charge_state = max(self.data.get.currents) > self.STANDBY_THRESHOLD
self.process_on_time()

# power ist bei andauerndem Fehler bereits vom Modul auf 0 gesetzt; error_timer wird hier
# nur noch für das Schaltbefehl-Gate in get_parameter() getickt.
self.data.set.error_timer = tick_error_timer(self.data.get.fault_state, self.data.set.error_timer)
if self.data.usage.type in NOT_CONTROLLED:
return
else:
Comment thread
seaspotter marked this conversation as resolved.
Expand Down Expand Up @@ -121,6 +126,10 @@ def is_switch_interval_elapsed(self):
"Verbraucher nicht abgeschaltet werden darf.")

def get_parameter(self) -> Tuple[float, Optional[str], Optional[Chargemode], Chargemode]:
if error_duration_exceeded(self.data.get.fault_state, self.data.set.error_timer):
message = (f"Fehler beim Auslesen des Verbrauchers {get_component_name_by_id(self.num)}. "
"Es wird nicht mehr angesteuert.")
return (0, message, self.data.control_parameter.chargemode, Chargemode.STOP)
if self.data.set.switch_interval_elapsed is False:
log.debug("Intervall für neuen Schaltbefehl nicht abgelaufen.")
return (max(self.data.get.currents),
Expand Down
1 change: 1 addition & 0 deletions packages/control/consumer/consumer_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def get_consumer_sum(self):
try:
for consumer in data.data.consumer_data.values():
try:
# Fehlerfall ist bereits vom Modul selbst auf get.power abgebildet.
power = power + consumer.data.get.power
except Exception:
log.exception(f"Fehler in der allgemeinen Verbaucher-Klasse für Verbaucher {consumer}")
Expand Down
17 changes: 17 additions & 0 deletions packages/control/consumer/consumer_all_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from control import data
from control.consumer.consumer import Consumer
from control.consumer.consumer_all import AllConsumers


def test_get_consumer_sum_sums_power(data_):
# setup - Fehlerfall ist bereits vom Modul selbst abgebildet, get_consumer_sum() summiert nur noch
data.data.consumer_data = {"consumer1": Consumer(1), "consumer2": Consumer(2)}
data.data.consumer_data["consumer1"].data.get.power = 500
data.data.consumer_data["consumer2"].data.get.power = 2000
all_consumers = AllConsumers()

# execution
all_consumers.get_consumer_sum()

# evaluation
assert all_consumers.data.get.power == 2500
3 changes: 2 additions & 1 deletion packages/control/consumer/consumer_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class Get:
imported: float = 0
phases_in_use: int = 0
plug_state: bool = field(default=False, metadata={"topic": "get/plug_state"})
power: float = 0
power: float = field(default=0, metadata={"topic": "get/power"})
powers: Optional[List[Optional[float]]] = None
set_power: Optional[float] = None
state: Optional[bool] = False
Expand All @@ -83,6 +83,7 @@ class Get:
@dataclass
class Set:
current: float = field(default=0, metadata={"topic": "set/current"})
error_timer: Optional[float] = field(default=None, metadata={"topic": "set/error_timer"})
phases_to_use: int = field(default=1, metadata={"topic": "set/phases_to_use"})
plug_time: Optional[float] = field(default=None, metadata={"topic": "set/plug_time"})
required_power: float = field(default=0)
Expand Down
71 changes: 71 additions & 0 deletions packages/control/consumer/consumer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
from control.general import General
from control.bat_all import BatAll
from control.chargemode import Chargemode
from control.consumer import consumer as consumer_module
from control.consumer.consumer import Consumer
from control.consumer.consumer_data import WaitForStartStates
from control.consumer.usage import ConsumerUsage
from modules.common.fault_state_level import FaultStateLevel


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -102,6 +104,75 @@ def test_get_parameter_clears_previous_state_string_when_switch_interval_is_acti
assert state_string is None


def test_get_parameter_stops_consumer_after_60s_error(consumer: Consumer, monkeypatch):
# setup
error_timer = timecheck.create_timestamp() - 61
consumer.data.get.fault_state = FaultStateLevel.ERROR
consumer.data.set.error_timer = error_timer
consumer.data.set.switch_interval_elapsed = True
monkeypatch.setattr(consumer_module, "get_component_name_by_id", Mock(return_value="Verbraucher 1"))

# execution
required_current, message, mode, submode = consumer.get_parameter()

# evaluation
assert required_current == 0
assert submode == Chargemode.STOP
assert "Verbraucher 1" in message


def test_get_parameter_continues_normally_within_grace_period(consumer: Consumer):
# setup
# Fehler erst seit 30s - noch innerhalb der Gnadenfrist, get_parameter() darf noch nicht auf STOP gehen.
# switch_interval_elapsed=True, damit nur das neue Fehlerfall-Gate über den Rückgabewert entscheidet.
error_timer = timecheck.create_timestamp() - 30
consumer.data.get.fault_state = FaultStateLevel.ERROR
consumer.data.set.error_timer = error_timer
consumer.data.set.switch_interval_elapsed = True

# execution
_, _, _, submode = consumer.get_parameter()

# evaluation
assert submode != Chargemode.STOP


def test_update_ticks_error_timer_without_touching_already_zeroed_power(consumer: Consumer):
# setup
# Verbraucher seit über 60s im Fehlerzustand, power schon vom Modul genullt - update() fasst es nicht
# mehr an, tickt aber weiterhin error_timer für get_parameter().
error_timer = timecheck.create_timestamp() - 61
consumer.data.usage.type = ConsumerUsage.METER_ONLY # NOT_CONTROLLED, kurzer Rückweg nach der Zuordnung
consumer.data.get.power = 2000
consumer.data.get.fault_state = FaultStateLevel.ERROR
consumer.data.set.error_timer = error_timer

# execution
consumer.update()

# evaluation
assert consumer.data.get.power == 2000
assert consumer.data.set.error_timer == error_timer


def test_update_recovers_after_error_clears(consumer: Consumer):
# setup
# Verbraucher war im Fehlerzustand (error_timer noch gesetzt), liefert jetzt aber wieder gültige Werte
# (fault_state == 0) - der alte error_timer darf nicht mehr dazu führen, dass get_parameter() ihn
# weiterhin als fehlerhaft behandelt.
consumer.data.usage.type = ConsumerUsage.METER_ONLY
consumer.data.get.power = 1800
consumer.data.get.fault_state = 0
consumer.data.set.error_timer = timecheck.create_timestamp() - 61

# execution
consumer.update()

# evaluation
assert consumer.data.get.power == 1800
assert consumer.data.set.error_timer is None


@pytest.mark.parametrize(
("wait_for_start_active",
"state", "currents",
Expand Down
8 changes: 4 additions & 4 deletions packages/control/counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from modules.common.utils.component_parser import get_component_name_by_id
from modules.common.fault_state import FaultStateLevel
from helpermodules.phase_handling import convert_cp_currents_to_evu_currents
from helpermodules.constants import NO_ERROR
from helpermodules.constants import COMPONENT_ERROR_DURATION, NO_ERROR
from helpermodules import timecheck
from dataclass_utils.factories import currents_list_factory, voltages_list_factory
from control.load_protocol import Load
Expand Down Expand Up @@ -133,8 +133,6 @@ class SwitchOnTexts:


class Counter:
MAX_EVU_ERROR_DURATION = 60

def __init__(self, index):
try:
self.data = CounterData()
Expand Down Expand Up @@ -162,7 +160,7 @@ def _get_loadmanagement_state(self) -> bool:
if self.data.set.error_timer is None:
self.data.set.error_timer = timecheck.create_timestamp()
return True
elif timecheck.check_timestamp(self.data.set.error_timer, self.MAX_EVU_ERROR_DURATION) is False:
elif timecheck.check_timestamp(self.data.set.error_timer, COMPONENT_ERROR_DURATION) is False:
for cp in connected_cps:
if self.num == data.data.counter_all_data.get_id_evu_counter():
data.data.cp_data[cp].set_state_and_log(
Expand Down Expand Up @@ -277,6 +275,8 @@ def _set_power_left(self, loadmanagement_available: bool) -> None:
# Wenn der Verbraucher nicht angesteuert werden darf, im LM als nicht veränderbaren Verbrauch
# berücksichtigen.
if consumer.data.set.switch_interval_elapsed:
# consumer.data.get.power ist bereits durch Consumer.update() auf den Fehlerfall
# abgebildet (0 nach COMPONENT_ERROR_DURATION s andauerndem Fehler).
power_raw -= consumer.data.get.power
else:
log.debug(f"Verbraucher {consumer.num} als unveränderlichen Verbrauch im LM "
Expand Down
Loading
Loading