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
49 changes: 27 additions & 22 deletions packages/control/consumer/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Callable, Dict, List, Optional, Tuple

from control import data
from control.consumer.usage import NOT_CONTROLLED
from control.consumer.usage import NOT_CONTROLLED, ON_OFF
from control.algorithm.utils import get_medium_charging_current
from control.chargemode import Chargemode
from control.chargepoint.chargepoint_state import CHARGING_STATES, ChargepointState
Expand All @@ -20,6 +20,7 @@

class Consumer(Load):
PAUSE_BETWEEN_WAIT_FOR_START_TEST_RUNS = 3600
STANDBY_THRESHOLD = 0.15 # 35 Watt pro Phase

def __init__(self, index: int):
self.num = index
Expand Down Expand Up @@ -57,14 +58,14 @@ def update(self):
self.data.get.voltages[i])
self.data.get.phases_in_use = self.data.config.connected_phases
self.data.set.phases_to_use = self.data.config.connected_phases
self.data.get.charge_state = True if self.data.get.power > 0 else False
self.data.get.charge_state = max(self.data.get.currents) > self.STANDBY_THRESHOLD
self.reset_chargemode_at_time()
self.is_switch_interval_elapsed()
min_current, required_current, message, mode, submode = self.get_parameter()
required_current, message, mode, submode = self.get_parameter()
self.set_mode_changed(submode, mode)
if self.chargemode_changed or self.submode_changed:
data.data.counter_all_data.get_evu_counter().reset_switch_on_off(self)
self.set_control_parameter(min_current, required_current,
self.set_control_parameter(required_current,
self.data.config.connected_phases, submode, mode)
self.set_state_and_log(message)
self.process_on_time()
Expand All @@ -74,6 +75,15 @@ def update(self):
except Exception:
log.exception(f"Fehler bei Verbraucher {self.num}")

def _get_min_current(self) -> float:
if self.data.usage.type == ConsumerUsage.SUSPENDABLE_TUNABLE:
return self.data.config.min_current
else:
if self.data.get.charge_state:
return get_medium_charging_current(self.data.get.currents)
else:
return self._convert_power_to_current(self.data.config.max_power)
Comment on lines +82 to +85

def process_on_time(self):
if self.data.get.charge_state:
now = timecheck.create_timestamp()
Expand Down Expand Up @@ -105,19 +115,14 @@ def is_switch_interval_elapsed(self):
SURPLUS_CONTINOUS_STILL_RUNNING = ("Verbraucher läuft ggf auch ohne ausreichend Überschuss weiter, da der "
"Verbraucher nicht abgeschaltet werden darf.")

def get_parameter(self) -> Tuple[float, float, Optional[str], Optional[Chargemode], Chargemode]:
def get_parameter(self) -> Tuple[float, Optional[str], Optional[Chargemode], Chargemode]:
Comment thread
Copilot marked this conversation as resolved.
if self.data.set.switch_interval_elapsed is False:
log.debug("Intervall für neuen Schaltbefehl nicht abgelaufen.")
return (0,
0,
None,
self.data.control_parameter.chargemode,
self.data.control_parameter.submode)

if self.data.usage.type == ConsumerUsage.SUSPENDABLE_TUNABLE:
min_current = self.data.config.min_current
else:
min_current = self._convert_power_to_current(self.data.config.max_power)
required_current = 0
submode = Chargemode.STOP
mode = self.data.usage.chargemode
Expand All @@ -144,10 +149,10 @@ def get_parameter(self) -> Tuple[float, float, Optional[str], Optional[Chargemod
message = f"{message or ''} {tmp_message or ''}".strip()
if self.data.usage.chargemode == Chargemode.STOP:
required_current, message, submode = self.stop()
return min_current, required_current, message, mode, submode
return required_current, message, mode, submode

def _parse_required_current_by_usage(self, required_current: float) -> float:
if self.data.usage.type in [ConsumerUsage.CONTINUOUS, ConsumerUsage.SUSPENDABLE_ONOFF]:
if self.data.usage.type in ON_OFF:
if self.data.get.charge_state:
return get_medium_charging_current(self.data.get.currents)
else:
Expand Down Expand Up @@ -306,7 +311,7 @@ def convert_loading_hours_to_string(hour_list: List[datetime.datetime]) -> str:
else:
message = self.SCHEDULED_CHARGING_EXPENSIVE_HOUR.format(get_hours_message())
submode = Chargemode.PV_CHARGING
required_current = self._parse_required_current_by_usage(self.data.config.min_current)
required_current = self._parse_required_current_by_usage(self._get_min_current())
else:
now = datetime.datetime.today()
start_time = now + datetime.timedelta(seconds=remaining_time)
Expand All @@ -317,7 +322,7 @@ def convert_loading_hours_to_string(hour_list: List[datetime.datetime]) -> str:
message = self.SCHEDULED_CHARGING_USE_PV.format(
f"am {start_time.strftime('%d.%m')} um {start_time.strftime('%-H:%M')} Uhr")
submode = Chargemode.PV_CHARGING
required_current = self._parse_required_current_by_usage(self.data.config.min_current)
required_current = self._parse_required_current_by_usage(self._get_min_current())
return required_current, message, submode

TIME_CHARGING_MIN_BAT_SOC_REACHED = ("Betrieb mit Zeitladen nach Speicher-SoC nicht möglich, da der SoC des"
Expand Down Expand Up @@ -380,18 +385,18 @@ def eco_charging(self) -> Tuple[float, Optional[str], Chargemode]:
message = self.CHARGING_PRICE_LOW
submode = Chargemode.INSTANT_CHARGING
else:
required_current = self._parse_required_current_by_usage(self.data.config.min_current)
required_current = self._parse_required_current_by_usage(self._get_min_current())
message = self.CHARGING_PRICE_EXCEEDED
if self.data.control_parameter.state in CHARGING_STATES:
message += "Lädt mit Überschuss. "
submode = Chargemode.PV_CHARGING
else:
required_current = self._parse_required_current_by_usage(self.data.config.min_current)
required_current = self._parse_required_current_by_usage(self._get_min_current())
submode = Chargemode.PV_CHARGING
return required_current, message, submode

def pv_charging(self) -> Tuple[float, Optional[str], Chargemode]:
required_current = self._parse_required_current_by_usage(self.data.config.min_current)
required_current = self._parse_required_current_by_usage(self._get_min_current())
message = None
submode = Chargemode.PV_CHARGING
return required_current, message, submode
Expand All @@ -411,10 +416,11 @@ def wait_for_start_handler(
self, func: Callable[[], Tuple[float, Optional[str], Chargemode]]
) -> Tuple[float, Optional[str], Chargemode, bool]:
chargemode_defined_by_wait_for_start_handler = False
standby_threshold = self.STANDBY_THRESHOLD if self.data.usage.type in ON_OFF else self.data.config.min_current
if self.data.usage.wait_for_start_active:
if self.data.set.wait_for_start_state == WaitForStartStates.WAIT_FOR_DEVICE_START:
# mit Minimalstrom prüfen, damit Standby-Geräte nicht als laufend erkannt werden
if max(self.data.get.currents) > self.data.config.min_current:
if max(self.data.get.currents) > standby_threshold:
self.data.set.wait_for_start_state = WaitForStartStates.WAIT_FOR_STOPPED_DEVICE
required_current = 0
message = self.WAIT_FOR_STOPPED_DEVICE
Expand All @@ -428,7 +434,7 @@ def wait_for_start_handler(
chargemode_defined_by_wait_for_start_handler = True
elif self.data.set.wait_for_start_state == WaitForStartStates.WAIT_FOR_STOPPED_DEVICE:
# mit Minimalstrom prüfen, damit Standby-Geräte nicht als laufend erkannt werden
if max(self.data.get.currents) < self.data.config.min_current:
if max(self.data.get.currents) < standby_threshold:
self.data.set.wait_for_start_state = WaitForStartStates.DEVICE_WAITING_FOR_START
required_current, message, submode = func()
message = self.DEVICE_WAITING_FOR_START + " " + (message if message else "")
Expand All @@ -440,7 +446,7 @@ def wait_for_start_handler(
chargemode_defined_by_wait_for_start_handler = True
elif self.data.set.wait_for_start_state == WaitForStartStates.DEVICE_WAITING_FOR_START:
# mit Minimalstrom prüfen, damit Standby-Geräte nicht als laufend erkannt werden
if max(self.data.get.currents) > self.data.config.min_current:
if max(self.data.get.currents) > standby_threshold:
self.data.set.wait_for_start_state = WaitForStartStates.START_SIGNAL_RECEIVED
required_current, message, submode = func()
else:
Expand Down Expand Up @@ -481,12 +487,11 @@ def reset_chargemode_at_time(self):
self.data.usage.chargemode = self.data.usage.reset_chargemode.chargemode

def set_control_parameter(self,
min_current: float,
required_current: float,
phases: int,
submode: Chargemode,
mode: Optional[Chargemode]):
self.data.control_parameter.min_current = min_current
self.data.control_parameter.min_current = self._get_min_current()
self.data.control_parameter.required_current = required_current
self.data.control_parameter.phases = phases
self.data.control_parameter.submode = submode
Expand Down
73 changes: 72 additions & 1 deletion packages/control/consumer/consumer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def test_get_parameter_clears_previous_state_string_when_switch_interval_is_acti
consumer.data.set.state_str_prev = "previous state"

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

# evaluation
assert state_string is None
Expand Down Expand Up @@ -143,6 +143,45 @@ def test_wait_for_start_handler(
assert charging_func.call_count == func_calls


@pytest.mark.parametrize(
"usage_type, expected_result, expected_state",
[
pytest.param(
ConsumerUsage.SUSPENDABLE_ONOFF,
(0, Consumer.WAIT_FOR_STOPPED_DEVICE, Chargemode.STOP, True),
WaitForStartStates.WAIT_FOR_STOPPED_DEVICE,
id="on-off-uses-standby-threshold",
),
pytest.param(
ConsumerUsage.SUSPENDABLE_TUNABLE,
(10, Consumer.WAIT_FOR_DEVICE_START, Chargemode.INSTANT_CHARGING, True),
WaitForStartStates.WAIT_FOR_DEVICE_START,
id="tunable-uses-min-current-threshold",
),
],
)
def test_wait_for_start_handler_standby_threshold_depends_on_usage_type(
consumer: Consumer,
usage_type: ConsumerUsage,
expected_result: Tuple[float, str, Chargemode, bool],
expected_state: WaitForStartStates):
# setup
consumer.data.usage.wait_for_start_active = True
consumer.data.usage.type = usage_type
consumer.data.set.wait_for_start_state = WaitForStartStates.WAIT_FOR_DEVICE_START
# 0.2 A liegt über STANDBY_THRESHOLD (0.15), aber unter min_current (6 A)
consumer.data.get.currents = [0.2, 0.2, 0.2]
charging_func = Mock(return_value=(11, "ok", Chargemode.PV_CHARGING))

# execution
result = consumer.wait_for_start_handler(charging_func)

# evaluation
assert result == expected_result
assert consumer.data.set.wait_for_start_state == expected_state
assert charging_func.call_count == 0


@pytest.mark.parametrize(
"plans, plan_found, expected",
[
Expand Down Expand Up @@ -260,6 +299,38 @@ def test_pv_charging(consumer: Consumer):
assert ret == (10, None, Chargemode.PV_CHARGING)


@pytest.mark.parametrize(
"usage_type, charge_state, currents, expected_min_current",
[
pytest.param(ConsumerUsage.CONTINUOUS, True, [13, 13, 13], 13, id="continuous-while-running"),
pytest.param(ConsumerUsage.CONTINUOUS, False, [13, 13, 13], 10, id="continuous-while-stopped"),
pytest.param(ConsumerUsage.SUSPENDABLE_TUNABLE, True, [13, 13, 13], 6, id="tunable-while-running"),
pytest.param(ConsumerUsage.SUSPENDABLE_TUNABLE, False, [13, 13, 13], 6, id="tunable-while-stopped"),
],
)
def test_set_control_parameter_sets_min_current_by_usage_and_charge_state(
consumer: Consumer,
usage_type: ConsumerUsage,
charge_state: bool,
currents: List[float],
expected_min_current: float):
# setup
consumer.data.usage.type = usage_type
consumer.data.get.charge_state = charge_state
consumer.data.get.currents = currents

# execution
consumer.set_control_parameter(
required_current=11,
phases=1,
submode=Chargemode.PV_CHARGING,
mode=Chargemode.PV_CHARGING,
)

# evaluation
assert consumer.data.control_parameter.min_current == expected_min_current


@pytest.mark.parametrize(
"on_time, plan_duration, diff_end_date, expected",
[
Expand Down
1 change: 1 addition & 0 deletions packages/control/consumer/usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ class ConsumerUsage(Enum):


NOT_CONTROLLED = (ConsumerUsage.METER_ONLY, ConsumerUsage.SELF_CONTROLLED)
ON_OFF = (ConsumerUsage.SUSPENDABLE_ONOFF, ConsumerUsage.CONTINUOUS)
Loading