Skip to content
331 changes: 264 additions & 67 deletions packages/control/consumer/consumer.py

Large diffs are not rendered by default.

53 changes: 38 additions & 15 deletions packages/control/consumer/consumer_data.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import Callable, Dict, List, Optional, Union
from control.chargemode import Chargemode
from control.chargepoint.control_parameter import ControlParameter, control_parameter_factory
Expand Down Expand Up @@ -31,6 +32,31 @@ class TimeCharging:
plans: List[TimeChargingPlanConsumer] = field(default_factory=empty_list_factory)


class WaitForStartStates(Enum):
WAIT_FOR_DEVICE_START = "wait_for_device_start"
WAIT_FOR_STOPPED_DEVICE = "wait_for_stopped_device"
DEVICE_WAITING_FOR_START = "device_waiting_for_start"
START_SIGNAL_RECEIVED = "start_signal_received"


class ResetModes(Enum):
NEVER = "never"
MIDNIGHT = "midnight"
TIME = "time"


@dataclass
class ResetChargemode:
mode: ResetModes = ResetModes.NEVER
time: Optional[int] = None
chargemode: Chargemode = Chargemode.INSTANT_CHARGING


@dataclass
class Log:
on_time: float = 0


@dataclass
class MeterOnlyConfig:
type: ConsumerUsage = ConsumerUsage.METER_ONLY
Expand All @@ -39,37 +65,34 @@ class MeterOnlyConfig:
@dataclass
class SuspendableTunableDeviceConfig:
chargemode: Chargemode = Chargemode.INSTANT_CHARGING
min_current: float = 0.5
min_intervall: int = 60
eco_charging: EcoCharging = field(default_factory=lambda: EcoCharging())
scheduled_charging: ScheduledSuspendableCharging = field(default_factory=lambda: ScheduledSuspendableCharging())
time_charging: TimeCharging = field(default_factory=lambda: TimeCharging())
type: ConsumerUsage = ConsumerUsage.SUSPENDABLE_TUNABLE
reset_chargemode: ResetChargemode = field(default_factory=lambda: ResetChargemode())
wait_for_start_active: bool = True


@dataclass
class SuspendableOnOffDeviceConfig:
chargemode: Chargemode = Chargemode.INSTANT_CHARGING
min_current: float = 0.5
min_intervall: int = 60
eco_charging: EcoCharging = field(default_factory=lambda: EcoCharging())
scheduled_charging: ScheduledSuspendableCharging = field(default_factory=lambda: ScheduledSuspendableCharging())
time_charging: TimeCharging = field(default_factory=lambda: TimeCharging())
type: ConsumerUsage = ConsumerUsage.SUSPENDABLE_ONOFF
reset_chargemode: ResetChargemode = field(default_factory=lambda: ResetChargemode())
wait_for_start_active: bool = True


@dataclass
class ContinuousDeviceConfig:
chargemode: Chargemode = Chargemode.INSTANT_CHARGING
min_current: float = 0.5
min_intervall: int = 2*60*60
eco_charging: EcoCharging = field(default_factory=lambda: EcoCharging())
scheduled_charging: ScheduledContinuousCharging = field(default_factory=lambda: ScheduledContinuousCharging())
time_charging: TimeCharging = field(default_factory=lambda: TimeCharging())
type: ConsumerUsage = ConsumerUsage.CONTINUOUS
wait_for_start_active: bool = True
reset_chargemode: ResetChargemode = field(default_factory=lambda: ResetChargemode())


GET_DEFAULTS_BY_USAGE: Dict[ConsumerUsage, Union[MeterOnlyConfig,
Expand All @@ -89,7 +112,7 @@ class ContinuousDeviceConfig:
ConsumerUsage.CONTINUOUS: ContinuousDeviceConfig,
}

get_plan_class_for_usage: Dict[ConsumerUsage, Callable] = {
GET_PLAN_CLASS_FOR_USAGE: Dict[ConsumerUsage, Callable] = {
ConsumerUsage.SUSPENDABLE_TUNABLE: SuspendableScheduledPlanConsumer,
ConsumerUsage.SUSPENDABLE_ONOFF: SuspendableScheduledPlanConsumer,
ConsumerUsage.CONTINUOUS: ContinuousScheduledPlanConsumer,
Expand All @@ -101,6 +124,8 @@ class ConsumerConfig:
connected_phases: int = 1
phase_1: int = 1
max_power: float = 5000
min_current: float = 0.5
min_intervall: int = 60


@dataclass
Expand All @@ -124,21 +149,19 @@ class Get:

@dataclass
class Set:
current: float = 0
current: float = field(default=0, metadata={"topic": "set/current"})
loadmanagement_available: bool = False
phases_to_use: int = 0
plug_time: Optional[float] = 0
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 = 0
current_prev: float = 0
target_current: float = 0
charge_state_prev: bool = False
log: Log = field(default_factory=lambda: Log())
power: Optional[float] = None
timestamp_last_current_set: float = field(default=0, metadata={"topic": "set/timestamp_last_current_set"})
wait_for_start_signal_received: bool = field(
default=False, metadata={"topic": "set/wait_for_start_signal_received"})
wait_for_start_test_running: bool = field(default=False, metadata={"topic": "set/wait_for_start_test_running"})
wait_for_start_last_test_timestamp: float = field(
default=0, metadata={"topic": "set/wait_for_start_last_test_timestamp"})
wait_for_start_state: WaitForStartStates = field(
default=WaitForStartStates.WAIT_FOR_DEVICE_START, metadata={"topic": "set/wait_for_start_state"})


@dataclass
Expand Down
121 changes: 121 additions & 0 deletions packages/control/consumer/consumer_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from typing import Optional, Tuple
from unittest.mock import Mock

import pytest

from control import data
from control.chargemode import Chargemode
from control.consumer.consumer import Consumer
from control.consumer.consumer_data import ContinuousDeviceConfig, WaitForStartStates


@pytest.fixture(autouse=True)
def mock_data() -> None:
data.data_init(Mock())


@pytest.fixture
def consumer() -> Consumer:
load = Consumer(1)
load.data.usage = ContinuousDeviceConfig()
load.data.config.min_current = 6
load.data.config.max_power = 2300
load.data.config.connected_phases = 1
load.data.get.voltages = [230]
return load


@pytest.mark.parametrize(
("wait_for_start_active",
"state", "charge_state",
"func_result",
"expected_result",
"expected_state",
"func_calls"),
[
pytest.param(
False,
WaitForStartStates.WAIT_FOR_DEVICE_START,
False,
(11, "ok", Chargemode.ECO_CHARGING, Chargemode.PV_CHARGING),
(11, "ok", Chargemode.ECO_CHARGING, Chargemode.PV_CHARGING),
WaitForStartStates.WAIT_FOR_DEVICE_START,
1,
id="wait-for-start-disabled-passthrough",
),
pytest.param(
True,
WaitForStartStates.WAIT_FOR_DEVICE_START,
True,
(9, "running", Chargemode.PV_CHARGING, Chargemode.PV_CHARGING),
(0, Consumer.WAIT_FOR_STOPPED_DEVICE, Chargemode.STOP, Chargemode.STOP),
WaitForStartStates.WAIT_FOR_STOPPED_DEVICE,
0,
id="wait-for-device-start-charge-detected",
),
pytest.param(
True,
WaitForStartStates.WAIT_FOR_DEVICE_START,
False,
(9, "running", Chargemode.PV_CHARGING, Chargemode.PV_CHARGING),
(6, Consumer.WAIT_FOR_DEVICE_START, Chargemode.INSTANT_CHARGING, Chargemode.INSTANT_CHARGING),
WaitForStartStates.WAIT_FOR_DEVICE_START,
0,
id="wait-for-device-start-no-charge",
),
pytest.param(
True,
WaitForStartStates.WAIT_FOR_STOPPED_DEVICE,
False,
(8, "pv ok", Chargemode.PV_CHARGING, Chargemode.PV_CHARGING),
(8, f"{Consumer.DEVICE_WAITING_FOR_START} pv ok", Chargemode.PV_CHARGING, Chargemode.PV_CHARGING),
WaitForStartStates.DEVICE_WAITING_FOR_START,
1,
id="wait-for-stopped-device-started-waiting",
),
pytest.param(
True,
WaitForStartStates.DEVICE_WAITING_FOR_START,
True,
(7, "started", Chargemode.ECO_CHARGING, Chargemode.INSTANT_CHARGING),
(7, "started", Chargemode.ECO_CHARGING, Chargemode.INSTANT_CHARGING),
WaitForStartStates.START_SIGNAL_RECEIVED,
1,
id="device-waiting-start-signal-received",
),
pytest.param(
True,
WaitForStartStates.START_SIGNAL_RECEIVED,
True,
(10, "continue", Chargemode.PV_CHARGING, Chargemode.PV_CHARGING),
(10, "continue", Chargemode.PV_CHARGING, Chargemode.PV_CHARGING),
WaitForStartStates.START_SIGNAL_RECEIVED,
1,
id="start-signal-received-passthrough",
),
],
)
def test_wait_for_start_handler(
consumer: Consumer,
wait_for_start_active: bool,
state: WaitForStartStates,
charge_state: bool,
func_result: Tuple[int, str, Chargemode, Optional[Chargemode]],
expected_result: Tuple[int, str, Chargemode, Optional[Chargemode]],
expected_state: WaitForStartStates,
func_calls: int,
):
# setup
assert isinstance(consumer.data.usage, ContinuousDeviceConfig)
consumer.data.usage.wait_for_start_active = wait_for_start_active
consumer.data.set.wait_for_start_state = state
consumer.data.get.charge_state = charge_state
charging_func = Mock(return_value=func_result)

# 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 == func_calls
6 changes: 3 additions & 3 deletions packages/helpermodules/abstract_plans.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,17 @@ class AutolockPlan(TimeframePlan):
class TimeChargingPlanConsumer(TimeframePlan):
id: Optional[int] = None
min_bat_soc: Optional[int] = None
name: str = "neuer Verbraucher-Plan"
name: str = "neuer Zeit-Plan"


@dataclass
class SuspendableScheduledPlanConsumer(TimeframePlan):
id: Optional[int] = None
name: str = "neuer Verbraucher-Plan"
name: str = "neuer Ziel-Plan"
duration: int = 60


@dataclass
class ContinuousScheduledPlanConsumer(TimeframePlan):
id: Optional[int] = None
name: str = "neuer Verbraucher-Plan"
name: str = "neuer Ziel-Plan"
Loading
Loading