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
31 changes: 14 additions & 17 deletions packages/control/consumer/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from control.algorithm.utils import get_medium_charging_current
from control.chargemode import Chargemode
from control.chargepoint.chargepoint_state import CHARGING_STATES, ChargepointState
from control.consumer.consumer_data import ConsumerData, ConsumerUsage, ResetModes, WaitForStartStates
from control.consumer.consumer_data import ConsumerData, ConsumerUsage, WaitForStartStates
from control.load_protocol import Load
from control.text import format_next_time_charging_start
from helpermodules import timecheck
Expand Down Expand Up @@ -465,26 +465,23 @@ def reset_wait_for_start(self):
self.data.set.wait_for_start_state = WaitForStartStates.WAIT_FOR_DEVICE_START

def midnight_handler(self):
self.reset_chargemode_at_midnight()
self.reset_wait_for_start()
self.reset_on_time()

def reset_chargemode_at_midnight(self):
if self.data.usage.reset_chargemode.mode == ResetModes.MIDNIGHT:
if self.data.usage.chargemode != self.data.usage.reset_chargemode.chargemode:
log.info(f"Zurücksetzen des Lademodus auf {self.data.usage.reset_chargemode.chargemode} "
f"für Verbraucher {self.num} um Mitternacht.")
self.data.usage.chargemode = self.data.usage.reset_chargemode.chargemode

def reset_chargemode_at_time(self):
if (self.data.usage.reset_chargemode.mode == ResetModes.TIME and
self.data.usage.reset_chargemode.time is not None):
if (self.data.usage.reset_chargemode.time < timecheck.create_timestamp() <
self.data.usage.reset_chargemode.time + data.data.general_data.data.control_interval):
if self.data.usage.chargemode != self.data.usage.reset_chargemode.chargemode:
log.info(f"Zurücksetzen des Lademodus auf {self.data.usage.reset_chargemode.chargemode} "
f"für Verbraucher {self.num} um definierte Zeit.")
self.data.usage.chargemode = self.data.usage.reset_chargemode.chargemode
if self.data.usage.reset_chargemode.active is False:
return

control_interval = data.data.general_data.data.control_interval
reset_window_start = -control_interval
remaining_time = timecheck.check_end_time_current_occurrence(self.data.usage.reset_chargemode)
is_within_reset_window = reset_window_start < remaining_time <= 0

if (is_within_reset_window and
self.data.usage.chargemode != self.data.usage.reset_chargemode.chargemode):
log.info(f"Zurücksetzen des Lademodus auf {self.data.usage.reset_chargemode.chargemode} "
f"für Verbraucher {self.num} um definierte Zeit.")
self.data.usage.chargemode = self.data.usage.reset_chargemode.chargemode

def set_control_parameter(self,
required_current: float,
Expand Down
13 changes: 3 additions & 10 deletions packages/control/consumer/consumer_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from control.chargepoint.control_parameter import ControlParameter, control_parameter_factory
from control.consumer.usage import ConsumerUsage
from dataclass_utils.factories import empty_list_factory, voltages_list_factory
from helpermodules.abstract_plans import ScheduledPlanConsumer, TimeChargingPlanConsumer
from helpermodules.abstract_plans import ScheduledPlanConsumer, TimeChargingPlanConsumer, TimepointPlan
from helpermodules.constants import NO_ERROR
from modules.common.consumer_setup import ConsumerSetup

Expand Down Expand Up @@ -33,16 +33,9 @@ class WaitForStartStates(Enum):
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
class ResetChargemode(TimepointPlan):
active: bool = False
chargemode: Chargemode = Chargemode.INSTANT_CHARGING


Expand Down
68 changes: 68 additions & 0 deletions packages/control/consumer/consumer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,71 @@ def test_scheduled_charging_calc_current_electricity_tariff(

# evaluation
assert ret == expected


@pytest.mark.parametrize(
"is_active,current_mode,target_mode,remaining_time,expected_mode,expected_check_end_time_calls",
[
pytest.param(
False,
Chargemode.STOP,
Chargemode.PV_CHARGING,
0,
Chargemode.STOP,
0,
id="inactive-noop",
),
pytest.param(
True,
Chargemode.STOP,
Chargemode.PV_CHARGING,
5,
Chargemode.STOP,
1,
id="active-not-due-noop",
),
pytest.param(
True,
Chargemode.STOP,
Chargemode.PV_CHARGING,
0,
Chargemode.PV_CHARGING,
1,
id="active-due-switch",
),
pytest.param(
True,
Chargemode.PV_CHARGING,
Chargemode.PV_CHARGING,
0,
Chargemode.PV_CHARGING,
1,
id="active-due-already-target",
),
],
)
def test_reset_chargemode_at_time(
consumer: Consumer,
monkeypatch: pytest.MonkeyPatch,
is_active: bool,
current_mode: Chargemode,
target_mode: Chargemode,
remaining_time: int,
expected_mode: Chargemode,
expected_check_end_time_calls: int,
):
# setup
consumer.data.usage.chargemode = current_mode
consumer.data.usage.reset_chargemode.active = is_active
consumer.data.usage.reset_chargemode.chargemode = target_mode
check_end_time_mock = Mock(return_value=remaining_time)
monkeypatch.setattr(timecheck, "check_end_time_current_occurrence", check_end_time_mock)

# execution
consumer.reset_chargemode_at_time()

# evaluation
assert consumer.data.usage.chargemode == expected_mode
assert check_end_time_mock.call_count == expected_check_end_time_calls
if expected_check_end_time_calls:
check_end_time_mock.assert_called_once_with(consumer.data.usage.reset_chargemode)
10 changes: 7 additions & 3 deletions packages/helpermodules/abstract_plans.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,23 @@ class TimeframePlan(PlanBase):


@dataclass
class ScheduledChargingPlan(PlanBase):
class TimepointPlan(PlanBase):
time: str = "07:00"
frequency: FrequencyDate = field(default_factory=frequency_date_factory)


@dataclass
class ScheduledChargingPlan(TimepointPlan):
bidi_charging_enabled: bool = False
bidi_power: int = 10000
current: int = 14
dc_current: float = 145
et_active: bool = False
frequency: FrequencyDate = field(default_factory=frequency_date_factory)
id: Optional[int] = None
name: str = "neuer Zielladen-Plan"
limit: ScheduledLimit = field(default_factory=scheduled_limit_factory)
phases_to_use: int = 0
phases_to_use_pv: int = 0
time: str = "07:00" # ToDo: aktuelle Zeit verwenden


@dataclass
Expand Down
33 changes: 30 additions & 3 deletions packages/helpermodules/timecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from dateutil.relativedelta import relativedelta

from helpermodules.abstract_plans import (AutolockPlan, ScheduledChargingPlan, ScheduledPlanConsumer,
TimeChargingPlan, TimeChargingPlanConsumer)
TimeChargingPlan, TimeChargingPlanConsumer, TimepointPlan)

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -201,8 +201,8 @@ def _get_next_start_for_timeframe_plan(plan: T, now: datetime.datetime) -> Optio
return None


def check_end_time(plan: Union[ScheduledPlanConsumer, ScheduledChargingPlan, TimeChargingPlanConsumer],
buffer: Optional[float]) -> float:
def check_end_time(plan: Union[ScheduledPlanConsumer, ScheduledChargingPlan, TimeChargingPlanConsumer, TimepointPlan],
buffer: float = 0) -> float:
""" gibt die verbleibende Zeit in Sekunden zurück.

Return
Expand Down Expand Up @@ -238,6 +238,33 @@ def check_end_time(plan: Union[ScheduledPlanConsumer, ScheduledChargingPlan, Tim
return remaining_time.total_seconds()


def check_end_time_current_occurrence(plan: TimepointPlan) -> float:
now = datetime.datetime.today()
end = datetime.datetime.strptime(plan.time, '%H:%M')

if plan.frequency.selected == "once":
end_date = datetime.datetime.strptime(plan.frequency.once, "%Y-%m-%d")
end = end.replace(end_date.year, end_date.month, end_date.day)
elif plan.frequency.selected == "daily":
end = end.replace(now.year, now.month, now.day)
elif plan.frequency.selected == "weekly":
if not any(plan.frequency.weekly):
raise ValueError("Es muss mindestens ein Tag ausgewählt werden.")
end = end.replace(now.year, now.month, now.day)
today_weekday = now.weekday()
if not plan.frequency.weekly[today_weekday]:
days_to_previous_selected = next(
day_offset
for day_offset in range(1, 7)
if plan.frequency.weekly[(today_weekday - day_offset) % 7]
)
end = end - datetime.timedelta(days=days_to_previous_selected)
else:
raise TypeError(f'Unbekannte Häufigkeit {plan.frequency.selected}')

return (end - now).total_seconds()


def _get_next_charging_day(weekly: List[bool], weekday: int) -> int:
count = 0
for i in range(weekday, len(weekly)):
Expand Down
34 changes: 34 additions & 0 deletions packages/helpermodules/timecheck_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,40 @@ def test_check_end_time(time: str,
assert remaining_time == expected_remaining_time


@pytest.mark.parametrize(
"time, selected, date,expected_remaining_time",
[
pytest.param("9:00", "once", "2022-05-16", 1148, id="once-future-not-due"),
pytest.param("12:00", "daily", [], 11948, id="daily-future-not-due"),
pytest.param("8:05", "daily", [], -2152, id="daily-past-due"),
pytest.param("9:00", "weekly", [True, False, False, False, False, False, False], 1148,
id="weekly-selected-today-future"),
pytest.param("8:05", "weekly", [True, False, False, False, False, False, False], -2152,
id="weekly-selected-today-past-due"),
pytest.param("9:00", "weekly", [False, False, False, False, False, False, True], -85252,
id="weekly-not-selected-today-use-previous-selected"),
],
)
def test_check_end_time_current_occurrence(
time: str,
selected: str,
date: Union[List[bool], str, List],
expected_remaining_time: float,
):
# setup
plan = Mock(spec=ScheduledChargingPlan, time=time, frequency=Mock(spec=FrequencyDate, selected=selected))
if selected == "weekly":
setattr(plan.frequency, "weekly", date)
elif selected == "once":
setattr(plan.frequency, "once", date)

# execution
remaining_time = timecheck.check_end_time_current_occurrence(plan)

# evaluation
assert remaining_time == expected_remaining_time


@pytest.mark.parametrize("weekday, weekly, expected_days",
[pytest.param(0, [True, True, False, False, False, False, False], 0),
pytest.param(1, [True, False, False, False, False, False, False], 6),
Expand Down
37 changes: 35 additions & 2 deletions packages/helpermodules/update_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from paho.mqtt.client import Client as MqttClient, MQTTMessage

from control.chargemode import Chargemode
from control.consumer.consumer_data import ResetChargemode
from control.consumer.usage import NOT_CONTROLLED
from control.limiting_value import LoadmanagementLimit

Expand All @@ -22,7 +23,7 @@
from helpermodules import hardware_configuration
from helpermodules import pub
from helpermodules.broker import BrokerClient
from helpermodules.abstract_plans import Limit
from helpermodules.abstract_plans import FrequencyDate, Limit
from helpermodules.constants import DEFAULT_COLORS, NO_ERROR
from helpermodules.hardware_configuration import (
get_hardware_configuration_setting,
Expand Down Expand Up @@ -62,7 +63,7 @@

class UpdateConfig:

DATASTORE_VERSION = 152
DATASTORE_VERSION = 153

FILE_OPERATION_VERSION = 0

Expand Down Expand Up @@ -4070,3 +4071,35 @@ def upgrade_datastore_152(self):
else:
remove_acl_role("consumer-<id>-write-access", consumer_id)
self._append_datastore_version(152)

def upgrade_datastore_153(self) -> None:
def upgrade(topic: str, payload) -> Optional[dict]:
if re.search(r"^openWB/consumer/[0-9]+/usage$", topic) is not None:
usage = decode_payload(payload)
reset_chargemode = usage["reset_chargemode"]
if reset_chargemode.get("mode") is not None:
legacy_target_chargemode = reset_chargemode.get("chargemode", Chargemode.INSTANT_CHARGING)
# altes Schema
if reset_chargemode["mode"] == "never":
reset_chargemode = ResetChargemode(active=False, chargemode=legacy_target_chargemode)
elif reset_chargemode["mode"] == "midnight":
reset_chargemode = ResetChargemode(active=True, time="00:00",
frequency=FrequencyDate(selected="daily"),
chargemode=legacy_target_chargemode)
elif reset_chargemode["mode"] == "time":
timestamp = reset_chargemode["time"]
if timestamp is not None:
date = datetime.datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d")
time = datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M")
else:
date = datetime.datetime.today().strftime("%Y-%m-%d")
time = "07:00"
reset_chargemode = ResetChargemode(
active=True, time=time, frequency=FrequencyDate(selected="once", once=date),
chargemode=legacy_target_chargemode)

usage["reset_chargemode"] = reset_chargemode
return {topic: asdict(usage)}
return None
self._loop_all_received_topics(upgrade)
self._append_datastore_version(153)
Loading
Loading