From 5890361fcb00ad86baa9aea971687f55e733a92c Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 16 Jan 2025 12:20:20 +0100 Subject: [PATCH 01/59] phases to use in instant charging --- packages/control/ev/charge_template.py | 31 +++++++++++-------------- packages/control/ev/ev.py | 2 +- packages/control/general.py | 11 --------- packages/helpermodules/subdata.py | 2 -- packages/helpermodules/update_config.py | 2 -- 5 files changed, 15 insertions(+), 33 deletions(-) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 7ab043963e..c7c7cb6abb 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -45,6 +45,7 @@ class InstantCharging: current: int = 10 dc_current: float = 145 limit: Limit = field(default_factory=limit_factory) + phases_to_use: int = 3 @dataclass @@ -185,36 +186,32 @@ def time_charging(self, def instant_charging(self, soc: Optional[float], imported_instant_charging: float, - charging_type: str) -> Tuple[int, str, Optional[str]]: + charging_type: str) -> Tuple[int, str, Optional[str], int]: """ prüft, ob die Lademengenbegrenzung erreicht wurde und setzt entsprechend den Ladestrom. """ message = None + sub_mode = "instant_charging" try: instant_charging = self.data.chargemode.instant_charging + phases = instant_charging.phases_to_use if charging_type == ChargingType.AC.value: current = instant_charging.current else: current = instant_charging.dc_current - if self.data.et.active and data.data.optional_data.et_provider_available(): - if not data.data.optional_data.et_charging_allowed(self.data.et.max_price): - return 0, "stop", self.CHARGING_PRICE_EXCEEDED - if instant_charging.limit.selected == "none": - return current, "instant_charging", message - elif instant_charging.limit.selected == "soc": + if instant_charging.limit.selected == "soc": if soc: - if soc < instant_charging.limit.soc: - return current, "instant_charging", message - else: - return 0, "stop", self.INSTANT_CHARGING_SOC_REACHED - else: - return current, "instant_charging", message + if soc > instant_charging.limit.soc: + current = 0 + sub_mode = "stop" + message = self.INSTANT_CHARGING_SOC_REACHED elif instant_charging.limit.selected == "amount": - if imported_instant_charging < self.data.chargemode.instant_charging.limit.amount: - return current, "instant_charging", message - else: - return 0, "stop", self.INSTANT_CHARGING_AMOUNT_REACHED + if imported_instant_charging > self.data.chargemode.instant_charging.limit.amount: + current = 0, + sub_mode = "stop" + message = self.INSTANT_CHARGING_AMOUNT_REACHED else: raise TypeError(f'{instant_charging.limit.selected} unbekanntes Sofortladen-Limit.') + return current, sub_mode, message, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) return 0, "stop", "Keine Ladung, da da ein interner Fehler aufgetreten ist: "+traceback.format_exc() diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 5f22826304..204206d5d0 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -206,7 +206,7 @@ def get_required_current(self, if control_parameter.imported_instant_charging is None: control_parameter.imported_instant_charging = imported used_amount = imported - control_parameter.imported_instant_charging - required_current, submode, message = self.charge_template.instant_charging( + required_current, submode, message, phases = self.charge_template.instant_charging( self.data.get.soc, used_amount, charging_type) diff --git a/packages/control/general.py b/packages/control/general.py index 8207891937..10f9b58aaf 100644 --- a/packages/control/general.py +++ b/packages/control/general.py @@ -13,16 +13,6 @@ log = logging.getLogger(__name__) -@dataclass -class InstantCharging: - phases_to_use: int = field(default=1, metadata={ - "topic": "chargemode_config/instant_charging/phases_to_use"}) - - -def instant_charging_factory() -> InstantCharging: - return InstantCharging() - - def control_range_factory() -> List: return [0, 230] @@ -87,7 +77,6 @@ def time_charging_factory() -> TimeCharging: @dataclass class ChargemodeConfig: - instant_charging: InstantCharging = field(default_factory=instant_charging_factory) phase_switch_delay: int = field(default=5, metadata={ "topic": "chargemode_config/phase_switch_delay"}) pv_charging: PvCharging = field(default_factory=pv_charging_factory) diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index f843a7ceb2..e71284c77b 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -597,8 +597,6 @@ def process_general_topic(self, var: general.General, msg: mqtt.MQTTMessage): elif re.search("/general/chargemode_config/", msg.topic) is not None: if re.search("/general/chargemode_config/pv_charging/", msg.topic) is not None: self.set_json_payload_class(var.data.chargemode_config.pv_charging, msg) - elif re.search("/general/chargemode_config/instant_charging/", msg.topic) is not None: - self.set_json_payload_class(var.data.chargemode_config.instant_charging, msg) elif re.search("/general/chargemode_config/scheduled_charging/", msg.topic) is not None: self.set_json_payload_class(var.data.chargemode_config.scheduled_charging, msg) elif re.search("/general/chargemode_config/time_charging/", msg.topic) is not None: diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 992ad9c80b..591fb6a7b3 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -225,7 +225,6 @@ class UpdateConfig: "^openWB/general/chargemode_config/retry_failed_phase_switches$", "^openWB/general/chargemode_config/scheduled_charging/phases_to_use$", "^openWB/general/chargemode_config/scheduled_charging/phases_to_use_pv$", - "^openWB/general/chargemode_config/instant_charging/phases_to_use$", "^openWB/general/chargemode_config/time_charging/phases_to_use$", # obsolet, Daten hieraus müssen nach prices/ überführt werden "^openWB/general/price_kwh$", @@ -471,7 +470,6 @@ class UpdateConfig: min_current=10))), ("openWB/vehicle/template/charge_template/0", get_charge_template_default()), ("openWB/general/charge_log_data_config", get_default_charge_log_columns()), - ("openWB/general/chargemode_config/instant_charging/phases_to_use", 3), ("openWB/general/chargemode_config/pv_charging/bat_mode", BatConsiderationMode.EV_MODE.value), ("openWB/general/chargemode_config/pv_charging/bat_power_discharge", 1000), ("openWB/general/chargemode_config/pv_charging/bat_power_discharge_active", True), From 88ff3f8b2a2060fb9901dd300dc089c116b076b8 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 16 Jan 2025 12:57:00 +0100 Subject: [PATCH 02/59] pv --- packages/control/chargepoint/chargepoint.py | 22 ++--- packages/control/ev/charge_template.py | 91 +++++++++++---------- packages/control/ev/charge_template_test.py | 4 +- packages/control/ev/ev.py | 4 +- packages/control/general.py | 2 - packages/helpermodules/setdata.py | 3 +- packages/helpermodules/update_config.py | 2 - 7 files changed, 58 insertions(+), 70 deletions(-) diff --git a/packages/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index 4f3388c863..95b37f2a63 100644 --- a/packages/control/chargepoint/chargepoint.py +++ b/packages/control/chargepoint/chargepoint.py @@ -486,17 +486,9 @@ def initiate_phase_switch(self): except Exception: log.exception("Fehler in der Ladepunkt-Klasse von "+str(self.num)) - def get_phases_by_selected_chargemode(self) -> int: + def get_phases_by_selected_chargemode(self, phases_chargemode: int) -> int: charging_ev = self.data.set.charging_ev_data - # Zeitladen kann nicht als Lademodus ausgewählt werden. Ob Zeitladen aktiv ist, lässt sich aus dem Submode - # erkennen. - if self.data.control_parameter.submode == "time_charging": - mode = "time_charging" - else: - mode = charging_ev.charge_template.data.chargemode.selected - chargemode = data.data.general_data.get_phases_chargemode(mode, self.data.control_parameter.submode) - - if (chargemode is None or + if (phases_chargemode is None or (self.data.config.auto_phase_switch_hw is False and self.data.get.charge_state) or self.data.control_parameter.failed_phase_switches > self.MAX_FAILED_PHASE_SWITCHES): # Wenn keine Umschaltung verbaut ist, die Phasenzahl nehmen, mit der geladen wird. Damit werden zB auch @@ -505,7 +497,7 @@ def get_phases_by_selected_chargemode(self) -> int: elif self.data.control_parameter.state == ChargepointState.PERFORMING_PHASE_SWITCH: phases = self.data.set.phases_to_use log.debug(f"Umschaltung wird durchgeführt, Phasenzahl nicht ändern {phases}") - elif chargemode == 0: + elif phases_chargemode == 0: # Wenn die Lademodus-Phasen 0 sind, wird die bisher genutzte Phasenzahl weiter genutzt, # bis der Algorithmus eine Umschaltung vorgibt, zB weil der gewählte Lademodus eine # andere Phasenzahl benötigt oder bei PV-Laden die automatische Umschaltung aktiv ist. @@ -524,10 +516,10 @@ def get_phases_by_selected_chargemode(self) -> int: phases = self.data.config.connected_phases log.debug(f"Phasenzahl Lademodus: {phases}") else: - if chargemode == 0: + if phases_chargemode == 0: phases = self.data.control_parameter.phases else: - phases = chargemode + phases = phases_chargemode return phases def get_max_phase_hw(self) -> int: @@ -635,14 +627,14 @@ def update(self, ev_list: Dict[str, Ev]) -> None: try: charging_ev = self._get_charging_ev(vehicle, ev_list) max_phase_hw = self.get_max_phase_hw() - self.data.control_parameter.phases = min( - self.get_phases_by_selected_chargemode(), max_phase_hw) state, message_ev, submode, required_current, phases = charging_ev.get_required_current( self.data.control_parameter, self.data.get.imported, max_phase_hw, self.cp_ev_support_phase_switch(), self.template.data.charging_type) + self.data.control_parameter.phases = min( + self.get_phases_by_selected_chargemode(phases), max_phase_hw) phases = self.set_phases(phases) self._pub_connected_vehicle(charging_ev) required_current = self.chargepoint_module.add_conversion_loss_to_current(required_current) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index c7c7cb6abb..1f9107342b 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -52,11 +52,13 @@ class InstantCharging: class PvCharging: dc_min_current: float = 145 dc_min_soc_current: float = 145 - min_soc_current: int = 10 - min_current: int = 0 feed_in_limit: bool = False + limit: Limit = field(default_factory=limit_factory) + min_current: int = 0 + min_soc_current: int = 10 min_soc: int = 0 - max_soc: int = 100 + phases_to_use: int = 0 + phases_to_use_min_soc: int = 3 def pv_charging_factory() -> PvCharging: @@ -87,22 +89,11 @@ def chargemode_factory() -> Chargemode: return Chargemode() -@dataclass -class Et: - active: bool = False - max_price: float = 0.0002 - - -def et_factory() -> Et: - return Et() - - @dataclass class ChargeTemplateData: name: str = "Lade-Profil" prio: bool = False load_default: bool = False - et: Et = field(default_factory=et_factory) time_charging: TimeCharging = field(default_factory=time_charging_factory) chargemode: Chargemode = field(default_factory=chargemode_factory) @@ -180,8 +171,8 @@ def time_charging(self, log.exception("Fehler im ev-Modul "+str(self.ct_num)) return 0, "stop", "Keine Ladung, da da ein interner Fehler aufgetreten ist: "+traceback.format_exc(), None - INSTANT_CHARGING_SOC_REACHED = "Kein Sofortladen, da der Soc bereits erreicht wurde." - INSTANT_CHARGING_AMOUNT_REACHED = "Kein Sofortladen, da die Energiemenge bereits geladen wurde." + SOC_REACHED = "Keine Ladung, da der Soc bereits erreicht wurde." + AMOUNT_REACHED = "Keine Ladung, da die Energiemenge bereits geladen wurde." def instant_charging(self, soc: Optional[float], @@ -198,55 +189,67 @@ def instant_charging(self, current = instant_charging.current else: current = instant_charging.dc_current - if instant_charging.limit.selected == "soc": - if soc: - if soc > instant_charging.limit.soc: - current = 0 - sub_mode = "stop" - message = self.INSTANT_CHARGING_SOC_REACHED + + if instant_charging.limit.selected == "soc" and soc and soc > instant_charging.limit.soc: + current = 0 + sub_mode = "stop" + message = self.SOC_REACHED elif instant_charging.limit.selected == "amount": if imported_instant_charging > self.data.chargemode.instant_charging.limit.amount: current = 0, sub_mode = "stop" - message = self.INSTANT_CHARGING_AMOUNT_REACHED - else: - raise TypeError(f'{instant_charging.limit.selected} unbekanntes Sofortladen-Limit.') + message = self.AMOUNT_REACHED return current, sub_mode, message, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) return 0, "stop", "Keine Ladung, da da ein interner Fehler aufgetreten ist: "+traceback.format_exc() - PV_CHARGING_SOC_REACHED = "Keine Ladung, da der maximale Soc bereits erreicht wurde." PV_CHARGING_SOC_CHARGING = ("Ladung evtl. auch ohne PV-Überschuss, da der Mindest-SoC des Fahrzeugs noch nicht " "erreicht wurde.") PV_CHARGING_MIN_CURRENT_CHARGING = "Ladung evtl. auch ohne PV-Überschuss, da minimaler Dauerstrom aktiv ist." - def pv_charging(self, soc: Optional[float], min_current: int, charging_type: str) -> Tuple[int, str, Optional[str]]: + def pv_charging(self, + soc: Optional[float], + min_current: int, + charging_type: str) -> Tuple[int, str, Optional[str], int]: """ prüft, ob Min-oder Max-Soc erreicht wurden und setzt entsprechend den Ladestrom. """ message = None + sub_mode = "pv_charging" try: pv_charging = self.data.chargemode.pv_charging - if soc is None or soc < pv_charging.max_soc: - if pv_charging.min_soc != 0 and soc is not None: - if soc < pv_charging.min_soc: - if charging_type == ChargingType.AC.value: - current = pv_charging.min_soc_current - else: - current = pv_charging.dc_min_soc_current - return current, "instant_charging", self.PV_CHARGING_SOC_CHARGING - if charging_type == ChargingType.AC.value: - pv_min_current = pv_charging.min_current - else: - pv_min_current = pv_charging.dc_min_current - if pv_min_current == 0: + phases = pv_charging.phases_to_use + min_pv_current = (pv_charging.min_current if charging_type == ChargingType.AC.value + else pv_charging.dc_min_current) + if pv_charging.limit.selected == "soc" and soc and soc > pv_charging.limit.soc: + current = 0 + sub_mode = "stop" + message = self.SOC_REACHED + elif (pv_charging.limit.selected == "amount" and + pv_charging > self.data.chargemode.instant_charging.limit.amount): + current = 0, + sub_mode = "stop" + message = self.AMOUNT_REACHED + else: + if pv_charging.min_soc != 0 and soc is not None and soc < pv_charging.min_soc: + if charging_type == ChargingType.AC.value: + current = pv_charging.min_soc_current + else: + current = pv_charging.dc_min_soc_current + sub_mode = "instant_charging" + message = self.PV_CHARGING_SOC_CHARGING + phases = pv_charging.phases_to_use_min_soc + elif min_pv_current == 0: # nur PV; Ampere darf nicht 0 sein, wenn geladen werden soll - return min_current, "pv_charging", message + current = min_current + sub_mode = "pv_charging" else: # Min PV - return pv_min_current, "instant_charging", self.PV_CHARGING_MIN_CURRENT_CHARGING - else: - return 0, "stop", self.PV_CHARGING_SOC_REACHED + current = min_pv_current + sub_mode = "instant_charging" + message = self.PV_CHARGING_MIN_CURRENT_CHARGING + + return current, sub_mode, message, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) return 0, "stop", "Keine Ladung, da ein interner Fehler aufgetreten ist: "+traceback.format_exc() diff --git a/packages/control/ev/charge_template_test.py b/packages/control/ev/charge_template_test.py index 9b17b7c716..548c346ac9 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -76,10 +76,10 @@ def test_time_charging(plans: Dict[int, TimeChargingPlan], soc: float, used_amou pytest.param("none", 0, 0, (10, "instant_charging", None), id="without limit"), pytest.param("soc", None, 0, (10, "instant_charging", None), id="limit soc: soc not defined"), pytest.param("soc", 49, 0, (10, "instant_charging", None), id="limit soc: soc not reached"), - pytest.param("soc", 50, 0, (0, "stop", ChargeTemplate.INSTANT_CHARGING_SOC_REACHED), + pytest.param("soc", 50, 0, (0, "stop", ChargeTemplate.SOC_REACHED), id="limit soc: soc reached"), pytest.param("amount", 0, 999, (10, "instant_charging", None), id="limit amount: amount not reached"), - pytest.param("amount", 0, 1000, (0, "stop", ChargeTemplate.INSTANT_CHARGING_AMOUNT_REACHED), + pytest.param("amount", 0, 1000, (0, "stop", ChargeTemplate.AMOUNT_REACHED), id="limit amount: amount reached"), ]) def test_instant_charging(selected: str, current_soc: float, used_amount: float, diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 204206d5d0..21bd9ff117 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -211,7 +211,7 @@ def get_required_current(self, used_amount, charging_type) elif self.charge_template.data.chargemode.selected == "pv_charging": - required_current, submode, message = self.charge_template.pv_charging( + required_current, submode, message, phases = self.charge_template.pv_charging( self.data.get.soc, control_parameter.min_current, charging_type) elif self.charge_template.data.chargemode.selected == "standby": # Text von Zeit-und Zielladen nicht überschreiben. @@ -223,8 +223,6 @@ def get_required_current(self, required_current, submode, message = self.charge_template.stop() if submode == "stop" or submode == "standby" or (self.charge_template.data.chargemode.selected == "stop"): state = False - if phases is None: - phases = control_parameter.phases return state, message, submode, required_current, phases except Exception as e: log.exception("Fehler im ev-Modul "+str(self.num)) diff --git a/packages/control/general.py b/packages/control/general.py index 10f9b58aaf..3b32bcbe9b 100644 --- a/packages/control/general.py +++ b/packages/control/general.py @@ -29,8 +29,6 @@ class PvCharging: "topic": "chargemode_config/pv_charging/feed_in_yield"}) phase_switch_delay: int = field(default=7, metadata={ "topic": "chargemode_config/pv_charging/phase_switch_delay"}) - phases_to_use: int = field(default=1, metadata={ - "topic": "chargemode_config/pv_charging/phases_to_use"}) bat_power_discharge: int = field(default=1500, metadata={ "topic": "chargemode_config/pv_charging/bat_power_discharge"}) bat_power_discharge_active: bool = field(default=False, metadata={ diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 6b31c19f9e..5f15b6e64b 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -790,8 +790,7 @@ def process_general_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, int, [(5, 20)]) elif "openWB/set/general/chargemode_config/pv_charging/control_range" in msg.topic: self._validate_value(msg, int, collection=list) - elif (("openWB/set/general/chargemode_config/pv_charging/phases_to_use" in msg.topic or - "openWB/set/general/chargemode_config/scheduled_charging/phases_to_use" in msg.topic or + elif (("openWB/set/general/chargemode_config/scheduled_charging/phases_to_use" in msg.topic or "openWB/set/general/chargemode_config/scheduled_charging/phases_to_use_pv" in msg.topic)): self._validate_value(msg, int, [(0, 0), (1, 1), (3, 3)]) elif "openWB/set/general/chargemode_config/pv_charging/min_bat_soc" in msg.topic: diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 591fb6a7b3..0e576d8000 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -216,7 +216,6 @@ class UpdateConfig: "^openWB/general/chargemode_config/pv_charging/switch_off_delay$", "^openWB/general/chargemode_config/phase_switch_delay$", "^openWB/general/chargemode_config/pv_charging/control_range$", - "^openWB/general/chargemode_config/pv_charging/phases_to_use$", "^openWB/general/chargemode_config/pv_charging/min_bat_soc$", "^openWB/general/chargemode_config/pv_charging/bat_power_discharge$", "^openWB/general/chargemode_config/pv_charging/bat_power_discharge_active$", @@ -483,7 +482,6 @@ class UpdateConfig: ("openWB/general/chargemode_config/pv_charging/switch_on_threshold", 1500), ("openWB/general/chargemode_config/pv_charging/feed_in_yield", 0), ("openWB/general/chargemode_config/phase_switch_delay", 7), - ("openWB/general/chargemode_config/pv_charging/phases_to_use", 0), ("openWB/general/chargemode_config/retry_failed_phase_switches", ChargemodeConfig().retry_failed_phase_switches), ("openWB/general/chargemode_config/scheduled_charging/phases_to_use", 0), From 04e8e948e3e1264c4121bc442792011c46bee685 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 16 Jan 2025 14:29:16 +0100 Subject: [PATCH 03/59] draft scheudled charging --- packages/control/ev/charge_template.py | 82 ++++++++++----------- packages/control/ev/charge_template_test.py | 2 +- packages/control/general.py | 13 ---- packages/helpermodules/abstract_plans.py | 3 + 4 files changed, 44 insertions(+), 56 deletions(-) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 1f9107342b..8e67b1de45 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -110,7 +110,7 @@ class SelectedPlan: max_current: int = 16 missing_amount: float = 0 phases: int = 1 - id: int = 0 + plan: Optional[ScheduledChargingPlan] = None @dataclass @@ -259,7 +259,7 @@ def scheduled_charging_recent_plan(self, ev_template: EvTemplate, phases: int, used_amount: float, - max_phases: int, + max_hw_phasess: int, phase_switch_supported: bool, charging_type: str) -> Optional[SelectedPlan]: """ prüft, ob der Ziel-SoC oder die Ziel-Energiemenge erreicht wurde und stellt den zur Erreichung nötigen @@ -271,24 +271,19 @@ def scheduled_charging_recent_plan(self, max_current = ev_template.data.max_current_multi_phases else: max_current = ev_template.data.dc_max_current - instant_phases = data.data.general_data.get_phases_chargemode("scheduled_charging", "instant_charging") - if instant_phases == 0: - planned_phases = 3 - else: - planned_phases = instant_phases - planned_phases = min(planned_phases, max_phases) - plan_data = self._search_plan(max_current, soc, ev_template, planned_phases, used_amount, charging_type) - if (plan_data and + selected_plan = self._search_plan(max_current, soc, ev_template, min( + 3 if plan.phases_to_use == 0 else plan.phases_to_use, max_hw_phases), used_amount, charging_type) + if (selected_plan and charging_type == ChargingType.AC.value and - instant_phases == 0 and - plan_data.remaining_time > 300 and - self.data.et.active is False): + selected_plan.plan.phases_to_use == 0 and + selected_plan.remaining_time > 300 and + selected_plan.plan.et_active is False): max_current = ev_template.data.max_current_single_phase plan_data_single_phase = self._search_plan( max_current, soc, ev_template, 1, used_amount, charging_type) if plan_data_single_phase: if plan_data_single_phase.remaining_time > 0: - plan_data = plan_data_single_phase + selected_plan = plan_data_single_phase else: if charging_type == ChargingType.AC.value: if phases == 1: @@ -297,19 +292,19 @@ def scheduled_charging_recent_plan(self, max_current = ev_template.data.max_current_multi_phases else: max_current = ev_template.data.dc_max_current - plan_data = self._search_plan(max_current, soc, ev_template, phases, used_amount, charging_type) - return plan_data + selected_plan = self._search_plan(max_current, soc, ev_template, phases, used_amount, charging_type) + return selected_plan def _search_plan(self, max_current: int, soc: Optional[float], ev_template: EvTemplate, - phases: int, + max_hw_phases: int, used_amount: float, charging_type: str) -> Optional[SelectedPlan]: smallest_remaining_time = float("inf") missed_date_today_of_plan_with_smallest_remaining_time = False - plan_data: Optional[SelectedPlan] = None + selected_plan: Optional[SelectedPlan] = None battery_capacity = ev_template.data.battery_capacity for plan in self.data.chargemode.scheduled_charging.plans.values(): if plan.active: @@ -317,6 +312,7 @@ def _search_plan(self, raise ValueError("Um Zielladen mit SoC-Ziel nutzen zu können, bitte ein SoC-Modul konfigurieren " f"oder im Plan {plan.name} als Begrenzung Energie einstellen.") try: + phases = min(3 if plan.phases_to_use == 0 else plan.phases_to_use, max_hw_phases) duration, missing_amount = self._calculate_duration( plan, soc, battery_capacity, used_amount, phases, charging_type, ev_template) remaining_time, missed_date_today = timecheck.check_duration(plan, duration, self.BUFFER) @@ -337,19 +333,19 @@ def _search_plan(self, available_current = plan.current else: available_current = plan.dc_current - plan_data = SelectedPlan( + selected_plan = SelectedPlan( remaining_time=remaining_time, available_current=available_current, max_current=max_current, phases=phases, - id=plan.id, + plan=plan, missing_amount=missing_amount, duration=duration) log.debug(f"Plan-Nr. {plan.id}: Differenz zum Start {remaining_time}s, Dauer {duration/3600}h, " f"Termin heute verpasst: {missed_date_today}") except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) - return plan_data + return selected_plan def _calculate_duration(self, plan: ScheduledChargingPlan, @@ -395,7 +391,7 @@ def _calculate_duration(self, "Laden ist. {} Falls vorhanden, wird mit Überschuss geladen.") def scheduled_charging_calc_current(self, - plan_data: Optional[SelectedPlan], + selected_plan: Optional[SelectedPlan], soc: int, used_amount: float, control_parameter_phases: int, @@ -403,15 +399,15 @@ def scheduled_charging_calc_current(self, soc_request_interval_offset: int) -> Tuple[float, str, str, int]: current = 0 submode = "stop" - if plan_data is None: + if selected_plan is None: if len(self.data.chargemode.scheduled_charging.plans) == 0: return current, submode, self.SCHEDULED_CHARGING_NO_PLANS_CONFIGURED, control_parameter_phases else: return current, submode, self.SCHEDULED_CHARGING_NO_DATE_PENDING, control_parameter_phases - current_plan = self.data.chargemode.scheduled_charging.plans[str(plan_data.id)] - limit = current_plan.limit - phases = plan_data.phases - log.debug("Verwendeter Plan: "+str(current_plan.name)) + plan = selected_plan.plan + limit = plan.limit + phases = selected_plan.phases + log.debug("Verwendeter Plan: "+str(plan.name)) if limit.selected == "soc" and soc >= limit.soc_limit and soc >= limit.soc_scheduled: message = self.SCHEDULED_CHARGING_REACHED_LIMIT_SOC elif limit.selected == "soc" and limit.soc_scheduled <= soc < limit.soc_limit: @@ -420,48 +416,50 @@ def scheduled_charging_calc_current(self, submode = "pv_charging" # bei Überschuss-Laden mit der Phasenzahl aus den control_parameter laden, # um die Umschaltung zu berücksichtigen. - phases = control_parameter_phases + phases = plan.phases_to_use_pv elif limit.selected == "amount" and used_amount >= limit.amount: message = self.SCHEDULED_CHARGING_REACHED_AMOUNT - elif 0 - soc_request_interval_offset < plan_data.remaining_time < 300 + soc_request_interval_offset: + elif 0 - soc_request_interval_offset < selected_plan.remaining_time < 300 + soc_request_interval_offset: # 5 Min vor spätestem Ladestart if limit.selected == "soc": limit_string = self.SCHEDULED_CHARGING_LIMITED_BY_SOC.format(limit.soc_scheduled) else: limit_string = self.SCHEDULED_CHARGING_LIMITED_BY_AMOUNT.format(limit.amount/1000) message = self.SCHEDULED_CHARGING_IN_TIME.format( - plan_data.available_current, limit_string, current_plan.time) - current = plan_data.available_current + selected_plan.available_current, limit_string, plan.time) + current = selected_plan.available_current submode = "instant_charging" # weniger als die berechnete Zeit verfügbar # Ladestart wurde um maximal 20 Min verpasst. - elif plan_data.remaining_time <= 0 - soc_request_interval_offset: - if plan_data.duration + plan_data.remaining_time < 0: - current = plan_data.max_current + elif selected_plan.remaining_time <= 0 - soc_request_interval_offset: + if selected_plan.duration + selected_plan.remaining_time < 0: + current = selected_plan.max_current else: - current = min(plan_data.missing_amount/((plan_data.duration + plan_data.remaining_time) / - 3600)/(phases*230), plan_data.max_current) + current = min(selected_plan.missing_amount/((selected_plan.duration + selected_plan.remaining_time) / + 3600)/(phases*230), selected_plan.max_current) message = self.SCHEDULED_CHARGING_MAX_CURRENT.format(round(current, 2)) submode = "instant_charging" else: # Wenn dynamische Tarife aktiv sind, prüfen, ob jetzt ein günstiger Zeitpunkt zum Laden # ist. - if self.data.et.active and data.data.optional_data.et_provider_available(): - hour_list = data.data.optional_data.et_get_loading_hours(plan_data.duration, plan_data.remaining_time) + if plan.et_active and data.data.optional_data.et_provider_available(): + hour_list = data.data.optional_data.et_get_loading_hours( + selected_plan.duration, selected_plan.remaining_time) hours_message = ("Geladen wird zu folgenden Uhrzeiten: " + ", ".join([datetime.datetime.fromtimestamp(hour).strftime('%-H:%M') for hour in sorted(hour_list)]) + ".") + log.debug(f"Günstige Ladezeiten: {hour_list}") if timecheck.is_list_valid(hour_list): - message = self.SCHEDULED_CHARGING_CHEAP_HOUR.format(hours_message) - current = plan_data.available_current + message = self.SCHEDULED_CHARGING_CHEAP_HOUR + current = selected_plan.available_current submode = "instant_charging" elif ((limit.selected == "soc" and soc <= limit.soc_limit) or (limit.selected == "amount" and used_amount < limit.amount)): message = self.SCHEDULED_CHARGING_EXPENSIVE_HOUR.format(hours_message) current = min_current submode = "pv_charging" - phases = control_parameter_phases + phases = plan.phases_to_use_pv else: message = self.SCHEDULED_REACHED_LIMIT_SOC else: @@ -479,7 +477,7 @@ def scheduled_charging_calc_current(self, f"am {start_time.strftime('%d.%m')} um {start_time.strftime('%-H:%M')} Uhr") current = min_current submode = "pv_charging" - phases = control_parameter_phases + phases = plan.phases_to_use_pv return current, submode, message, phases def standby(self) -> Tuple[int, str, str]: diff --git a/packages/control/ev/charge_template_test.py b/packages/control/ev/charge_template_test.py index 548c346ac9..8439181b6d 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -213,7 +213,7 @@ def test_search_plan(check_duration_return1: Tuple[Optional[float], bool], if expected_plan_num is None: assert plan_data is None else: - assert plan_data.id == expected_plan_num + assert plan_data.plan.id == expected_plan_num assert plan_data.duration == 100 diff --git a/packages/control/general.py b/packages/control/general.py index 3b32bcbe9b..a5cddd7336 100644 --- a/packages/control/general.py +++ b/packages/control/general.py @@ -51,18 +51,6 @@ def pv_charging_factory() -> PvCharging: return PvCharging() -@dataclass -class ScheduledCharging: - phases_to_use: int = field(default=0, metadata={ - "topic": "chargemode_config/scheduled_charging/phases_to_use"}) - phases_to_use_pv: int = field(default=0, metadata={ - "topic": "chargemode_config/scheduled_charging/phases_to_use_pv"}) - - -def scheduled_charging_factory() -> ScheduledCharging: - return ScheduledCharging() - - @dataclass class TimeCharging: phases_to_use: int = field(default=1, metadata={ @@ -81,7 +69,6 @@ class ChargemodeConfig: retry_failed_phase_switches: bool = field( default=False, metadata={"topic": "chargemode_config/retry_failed_phase_switches"}) - scheduled_charging: ScheduledCharging = field(default_factory=scheduled_charging_factory) time_charging: TimeCharging = field(default_factory=time_charging_factory) unbalanced_load_limit: int = field( default=18, metadata={"topic": "chargemode_config/unbalanced_load_limit"}) diff --git a/packages/helpermodules/abstract_plans.py b/packages/helpermodules/abstract_plans.py index 60586a9944..7823178284 100644 --- a/packages/helpermodules/abstract_plans.py +++ b/packages/helpermodules/abstract_plans.py @@ -63,9 +63,12 @@ class TimeframePlan(PlanBase): class ScheduledChargingPlan(PlanBase): current: int = 14 dc_current: float = 145 + et_active: bool = False 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 From e7aa3d7ffe696ee7b2ac9ad72376ec197b1556a1 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 16 Jan 2025 14:53:28 +0100 Subject: [PATCH 04/59] time charging --- packages/control/ev/charge_template.py | 42 +++++++++++------------- packages/control/ev/ev.py | 6 ++-- packages/control/general.py | 11 ------- packages/helpermodules/abstract_plans.py | 3 +- packages/helpermodules/setdata.py | 3 -- packages/helpermodules/update_config.py | 2 -- 6 files changed, 25 insertions(+), 42 deletions(-) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 8e67b1de45..d7921f0b1e 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -132,41 +132,39 @@ class ChargeTemplate: def time_charging(self, soc: Optional[float], used_amount_time_charging: float, - charging_type: str) -> Tuple[int, str, Optional[str], Optional[str]]: + charging_type: str) -> Tuple[int, str, Optional[str], Optional[str], int]: """ prüft, ob ein Zeitfenster aktiv ist und setzt entsprechend den Ladestrom """ message = None + sub_mode = "time_charging" + id = None try: if self.data.time_charging.plans: plan = timecheck.check_plans_timeframe(self.data.time_charging.plans) if plan is not None: current = plan.current if charging_type == ChargingType.AC.value else plan.dc_current - if self.data.et.active and data.data.optional_data.et_provider_available(): - if not data.data.optional_data.et_charging_allowed(self.data.et.max_price): - return 0, "stop", self.CHARGING_PRICE_EXCEEDED, plan.id - if plan.limit.selected == "none": # kein Limit konfiguriert, mit konfigurierter Stromstärke laden - return current, "time_charging", message, plan.id - elif plan.limit.selected == "soc": # SoC Limit konfiguriert - if soc: - if soc < plan.limit.soc: - return current, "time_charging", message, plan.id # Limit nicht erreicht - else: - return 0, "stop", self.TIME_CHARGING_SOC_REACHED, plan.id # Limit erreicht - else: - return plan.current, "time_charging", message, plan.id - elif plan.limit.selected == "amount": # Energiemengenlimit konfiguriert - if used_amount_time_charging < plan.limit.amount: - return current, "time_charging", message, plan.id # Limit nicht erreicht - else: - return 0, "stop", self.TIME_CHARGING_AMOUNT_REACHED, plan.id # Limit erreicht - else: - raise TypeError(f'{plan.limit.selected} unbekanntes Zeitladen-Limit.') + phases = plan.phases_to_use + id = plan.id + if plan.limit.selected == "soc" and soc and soc > plan.limit.soc: + # SoC-Limit erreicht + current = 0 + sub_mode = "stop" + message = self.TIME_CHARGING_SOC_REACHED + elif plan.limit.selected == "amount" and used_amount_time_charging > plan.limit.amount: + # Energie-Limit erreicht + current = 0 + sub_mode = "stop" + message = self.TIME_CHARGING_AMOUNT_REACHED else: message = self.TIME_CHARGING_NO_PLAN_ACTIVE + current = 0 + sub_mode = "stop" else: message = self.TIME_CHARGING_NO_PLAN_CONFIGURED + current = 0 + sub_mode = "stop" log.debug(message) - return 0, "stop", message, None + return current, sub_mode, message, id, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) return 0, "stop", "Keine Ladung, da da ein interner Fehler aufgetreten ist: "+traceback.format_exc(), None diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 21bd9ff117..7f691a74e9 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -160,7 +160,7 @@ def get_required_current(self, # Wenn mit einem neuen Plan geladen wird, muss auch die Energiemenge von neuem gezählt werden. if (self.charge_template.data.chargemode.scheduled_charging.plans[str(plan_data.id)].limit. selected == "amount" and - plan_data.id != control_parameter.current_plan): + plan_data.plan.id != control_parameter.current_plan): control_parameter.imported_at_plan_start = imported # Wenn der SoC ein paar Minuten alt ist, kann der Termin trotzdem gehalten werden. # Zielladen kann nicht genauer arbeiten, als das Abfrageintervall vom SoC. @@ -168,7 +168,7 @@ def get_required_current(self, self.charge_template.data.chargemode. scheduled_charging.plans[str(plan_data.id)].limit.selected == "soc"): soc_request_interval_offset = self.soc_module.general_config.request_interval_charging - control_parameter.current_plan = plan_data.id + control_parameter.current_plan = plan_data.plan.id else: control_parameter.current_plan = None required_current, submode, message, phases = self.charge_template.scheduled_charging_calc_current( @@ -185,7 +185,7 @@ def get_required_current(self, if control_parameter.imported_at_plan_start is None: control_parameter.imported_at_plan_start = imported used_amount = imported - control_parameter.imported_at_plan_start - tmp_current, tmp_submode, tmp_message, plan_id = self.charge_template.time_charging( + tmp_current, tmp_submode, tmp_message, plan_id, phases = self.charge_template.time_charging( self.data.get.soc, used_amount, charging_type diff --git a/packages/control/general.py b/packages/control/general.py index a5cddd7336..bb6621828e 100644 --- a/packages/control/general.py +++ b/packages/control/general.py @@ -51,16 +51,6 @@ def pv_charging_factory() -> PvCharging: return PvCharging() -@dataclass -class TimeCharging: - phases_to_use: int = field(default=1, metadata={ - "topic": "chargemode_config/time_charging/phases_to_use"}) - - -def time_charging_factory() -> TimeCharging: - return TimeCharging() - - @dataclass class ChargemodeConfig: phase_switch_delay: int = field(default=5, metadata={ @@ -69,7 +59,6 @@ class ChargemodeConfig: retry_failed_phase_switches: bool = field( default=False, metadata={"topic": "chargemode_config/retry_failed_phase_switches"}) - time_charging: TimeCharging = field(default_factory=time_charging_factory) unbalanced_load_limit: int = field( default=18, metadata={"topic": "chargemode_config/unbalanced_load_limit"}) unbalanced_load: bool = field(default=False, metadata={ diff --git a/packages/helpermodules/abstract_plans.py b/packages/helpermodules/abstract_plans.py index 7823178284..fced1bc009 100644 --- a/packages/helpermodules/abstract_plans.py +++ b/packages/helpermodules/abstract_plans.py @@ -74,11 +74,12 @@ class ScheduledChargingPlan(PlanBase): @dataclass class TimeChargingPlan(TimeframePlan): - name: str = "neuer Zeitladen-Plan" current: int = 16 dc_current: float = 145 id: Optional[int] = None limit: Limit = field(default_factory=limit_factory) + name: str = "neuer Zeitladen-Plan" + phases_to_use: int = 1 @dataclass diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 5f15b6e64b..21e8a86a4c 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -790,9 +790,6 @@ def process_general_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, int, [(5, 20)]) elif "openWB/set/general/chargemode_config/pv_charging/control_range" in msg.topic: self._validate_value(msg, int, collection=list) - elif (("openWB/set/general/chargemode_config/scheduled_charging/phases_to_use" in msg.topic or - "openWB/set/general/chargemode_config/scheduled_charging/phases_to_use_pv" in msg.topic)): - self._validate_value(msg, int, [(0, 0), (1, 1), (3, 3)]) elif "openWB/set/general/chargemode_config/pv_charging/min_bat_soc" in msg.topic: self._validate_value(msg, int, [(0, 100)]) elif ("openWB/set/general/chargemode_config/pv_charging/bat_power_discharge" in msg.topic or diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 0e576d8000..d086780c42 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -224,7 +224,6 @@ class UpdateConfig: "^openWB/general/chargemode_config/retry_failed_phase_switches$", "^openWB/general/chargemode_config/scheduled_charging/phases_to_use$", "^openWB/general/chargemode_config/scheduled_charging/phases_to_use_pv$", - "^openWB/general/chargemode_config/time_charging/phases_to_use$", # obsolet, Daten hieraus müssen nach prices/ überführt werden "^openWB/general/price_kwh$", "^openWB/general/prices/bat$", @@ -486,7 +485,6 @@ class UpdateConfig: ChargemodeConfig().retry_failed_phase_switches), ("openWB/general/chargemode_config/scheduled_charging/phases_to_use", 0), ("openWB/general/chargemode_config/scheduled_charging/phases_to_use_pv", 0), - ("openWB/general/chargemode_config/time_charging/phases_to_use", 1), ("openWB/general/chargemode_config/unbalanced_load", False), ("openWB/general/chargemode_config/unbalanced_load_limit", 18), ("openWB/general/control_interval", 10), From 838c508cc2b3f820378fa50b101e1e7f20b3d80c Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 16 Jan 2025 15:35:24 +0100 Subject: [PATCH 05/59] eco --- packages/control/ev/charge_template.py | 51 +++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index d7921f0b1e..3e5ca32676 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -40,6 +40,15 @@ class TimeCharging: "topic": ""}) +@dataclass +class EcoCharging: + current: int = 6 + dc_current: float = 145 + limit: Limit = field(default_factory=limit_factory) + max_price: float = 0.0002 + phases_to_use: int = 3 + + @dataclass class InstantCharging: current: int = 10 @@ -61,6 +70,10 @@ class PvCharging: phases_to_use_min_soc: int = 3 +def eco_charging_factory() -> EcoCharging: + return EcoCharging() + + def pv_charging_factory() -> PvCharging: return PvCharging() @@ -76,6 +89,7 @@ def instant_charging_factory() -> InstantCharging: @dataclass class Chargemode: selected: str = "stop" + eco_charging: EcoCharging = field(default_factory=eco_charging_factory) pv_charging: PvCharging = field(default_factory=pv_charging_factory) scheduled_charging: ScheduledCharging = field(default_factory=scheduled_charging_factory) instant_charging: InstantCharging = field(default_factory=instant_charging_factory) @@ -122,7 +136,8 @@ class ChargeTemplate: "topic": ""}) BUFFER = -1200 # nach mehr als 20 Min Überschreitung wird der Termin als verpasst angesehen - CHARGING_PRICE_EXCEEDED = "Keine Ladung, da der aktuelle Strompreis über dem maximalen Strompreis liegt." + CHARGING_PRICE_EXCEEDED = "Keine Ladung, da der aktuelle Strompreis über dem maximalen Strompreis liegt. Falls vorhanden wird mit EVU-Überschuss geladen." + CHARGING_PRICE_LOW = "Ladung, da der aktuelle Strompreis unter dem maximalen Strompreis liegt." TIME_CHARGING_NO_PLAN_CONFIGURED = "Keine Ladung, da keine Zeitfenster für Zeitladen konfiguriert sind." TIME_CHARGING_NO_PLAN_ACTIVE = "Keine Ladung, da kein Zeitfenster für Zeitladen aktiv ist." @@ -252,6 +267,40 @@ def pv_charging(self, log.exception("Fehler im ev-Modul "+str(self.ct_num)) return 0, "stop", "Keine Ladung, da ein interner Fehler aufgetreten ist: "+traceback.format_exc() + def eco_charging(self, + soc: Optional[float], + min_current: int, + charging_type: str) -> Tuple[int, str, Optional[str], int]: + """ prüft, ob Min-oder Max-Soc erreicht wurden und setzt entsprechend den Ladestrom. + """ + message = None + sub_mode = "pv_charging" + try: + eco_charging = self.data.chargemode.eco_charging + phases = eco_charging.phases_to_use + current = eco_charging.current if charging_type == ChargingType.AC.value else eco_charging.dc_current + + if eco_charging.limit.selected == "soc" and soc and soc > eco_charging.limit.soc: + current = 0 + sub_mode = "stop" + message = self.SOC_REACHED + elif (eco_charging.limit.selected == "amount" and + eco_charging > self.data.chargemode.instant_charging.limit.amount): + current = 0, + sub_mode = "stop" + message = self.AMOUNT_REACHED + elif (data.data.optional_data.et_provider_available() and + data.data.optional_data.et_price_lower_than_limit(eco_charging.max_price)): + current = min_current + message = self.CHARGING_PRICE_EXCEEDED + else: + sub_mode = "instant_charging" + message = self.CHARGING_PRICE_LOW + return current, sub_mode, message, phases + except Exception: + log.exception("Fehler im ev-Modul "+str(self.ct_num)) + return 0, "stop", "Keine Ladung, da ein interner Fehler aufgetreten ist: "+traceback.format_exc() + def scheduled_charging_recent_plan(self, soc: float, ev_template: EvTemplate, From 25ad3ac8492b26471f1777d7988f151450faeb04 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 17 Jan 2025 09:48:36 +0100 Subject: [PATCH 06/59] fixes --- packages/control/algorithm/chargemodes.py | 14 ++++++++------ packages/control/chargemode.py | 2 +- packages/helpermodules/subdata.py | 6 ------ packages/helpermodules/update_config.py | 4 ---- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/control/algorithm/chargemodes.py b/packages/control/algorithm/chargemodes.py index 65e16a9645..4ea481c6a4 100644 --- a/packages/control/algorithm/chargemodes.py +++ b/packages/control/algorithm/chargemodes.py @@ -8,19 +8,21 @@ (None, Chargemode.TIME_CHARGING, False), (Chargemode.INSTANT_CHARGING, Chargemode.INSTANT_CHARGING, True), (Chargemode.INSTANT_CHARGING, Chargemode.INSTANT_CHARGING, False), + (Chargemode.ECO_CHARGING, Chargemode.INSTANT_CHARGING, True), + (Chargemode.ECO_CHARGING, Chargemode.INSTANT_CHARGING, False), (Chargemode.PV_CHARGING, Chargemode.INSTANT_CHARGING, True), (Chargemode.PV_CHARGING, Chargemode.INSTANT_CHARGING, False), (Chargemode.SCHEDULED_CHARGING, Chargemode.PV_CHARGING, True), (Chargemode.SCHEDULED_CHARGING, Chargemode.PV_CHARGING, False), + (Chargemode.ECO_CHARGING, Chargemode.PV_CHARGING, True), + (Chargemode.ECO_CHARGING, Chargemode.PV_CHARGING, False), (Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, True), (Chargemode.PV_CHARGING, Chargemode.PV_CHARGING, False), - (None, Chargemode.STANDBY, True), - (None, Chargemode.STANDBY, False), (None, Chargemode.STOP, True), (None, Chargemode.STOP, False)) -CONSIDERED_CHARGE_MODES_SURPLUS = CHARGEMODES[0:2] + CHARGEMODES[6:12] -CONSIDERED_CHARGE_MODES_PV_ONLY = CHARGEMODES[8:12] -CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT = CHARGEMODES[0:8] +CONSIDERED_CHARGE_MODES_SURPLUS = CHARGEMODES[0:2] + CHARGEMODES[6:14] +CONSIDERED_CHARGE_MODES_PV_ONLY = CHARGEMODES[10:14] +CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT = CHARGEMODES[0:10] CONSIDERED_CHARGE_MODES_MIN_CURRENT = CHARGEMODES[0:-1] -CONSIDERED_CHARGE_MODES_NO_CURRENT = CHARGEMODES[12:16] +CONSIDERED_CHARGE_MODES_NO_CURRENT = CHARGEMODES[14:16] diff --git a/packages/control/chargemode.py b/packages/control/chargemode.py index 6b0fada216..c9a7e41e88 100644 --- a/packages/control/chargemode.py +++ b/packages/control/chargemode.py @@ -6,5 +6,5 @@ class Chargemode(Enum): TIME_CHARGING = "time_charging" INSTANT_CHARGING = "instant_charging" PV_CHARGING = "pv_charging" - STANDBY = "standby" + ECO_CHARGING = "eco_charging" STOP = "stop" diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index e71284c77b..3a955a1ef5 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -597,12 +597,6 @@ def process_general_topic(self, var: general.General, msg: mqtt.MQTTMessage): elif re.search("/general/chargemode_config/", msg.topic) is not None: if re.search("/general/chargemode_config/pv_charging/", msg.topic) is not None: self.set_json_payload_class(var.data.chargemode_config.pv_charging, msg) - elif re.search("/general/chargemode_config/scheduled_charging/", msg.topic) is not None: - self.set_json_payload_class(var.data.chargemode_config.scheduled_charging, msg) - elif re.search("/general/chargemode_config/time_charging/", msg.topic) is not None: - self.set_json_payload_class(var.data.chargemode_config.time_charging, msg) - elif re.search("/general/chargemode_config/standby/", msg.topic) is not None: - self.set_json_payload_class(var.data.chargemode_config.standby, msg) else: self.set_json_payload_class(var.data.chargemode_config, msg) elif "openWB/general/extern" == msg.topic: diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index d086780c42..d22e363c20 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -222,8 +222,6 @@ class UpdateConfig: "^openWB/general/chargemode_config/pv_charging/bat_power_reserve$", "^openWB/general/chargemode_config/pv_charging/bat_power_reserve_active$", "^openWB/general/chargemode_config/retry_failed_phase_switches$", - "^openWB/general/chargemode_config/scheduled_charging/phases_to_use$", - "^openWB/general/chargemode_config/scheduled_charging/phases_to_use_pv$", # obsolet, Daten hieraus müssen nach prices/ überführt werden "^openWB/general/price_kwh$", "^openWB/general/prices/bat$", @@ -483,8 +481,6 @@ class UpdateConfig: ("openWB/general/chargemode_config/phase_switch_delay", 7), ("openWB/general/chargemode_config/retry_failed_phase_switches", ChargemodeConfig().retry_failed_phase_switches), - ("openWB/general/chargemode_config/scheduled_charging/phases_to_use", 0), - ("openWB/general/chargemode_config/scheduled_charging/phases_to_use_pv", 0), ("openWB/general/chargemode_config/unbalanced_load", False), ("openWB/general/chargemode_config/unbalanced_load_limit", 18), ("openWB/general/control_interval", 10), From cf83d446c2c028261e440f5ee4313e4ce4fc5a89 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 17 Jan 2025 14:40:09 +0100 Subject: [PATCH 07/59] fix --- packages/control/chargepoint/chargepoint.py | 2 -- packages/control/ev/charge_template.py | 3 --- packages/control/ev/ev.py | 9 ++------- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index 95b37f2a63..88859e45bd 100644 --- a/packages/control/chargepoint/chargepoint.py +++ b/packages/control/chargepoint/chargepoint.py @@ -633,8 +633,6 @@ def update(self, ev_list: Dict[str, Ev]) -> None: max_phase_hw, self.cp_ev_support_phase_switch(), self.template.data.charging_type) - self.data.control_parameter.phases = min( - self.get_phases_by_selected_chargemode(phases), max_phase_hw) phases = self.set_phases(phases) self._pub_connected_vehicle(charging_ev) required_current = self.chargepoint_module.add_conversion_loss_to_current(required_current) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 3e5ca32676..58b95eb0bf 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -527,8 +527,5 @@ def scheduled_charging_calc_current(self, phases = plan.phases_to_use_pv return current, submode, message, phases - def standby(self) -> Tuple[int, str, str]: - return 0, "standby", "Keine Ladung, da der Lademodus Standby aktiv ist." - def stop(self) -> Tuple[int, str, str]: return 0, "stop", "Keine Ladung, da der Lademodus Stop aktiv ist." diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 7f691a74e9..b1d4f156cd 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -213,15 +213,10 @@ def get_required_current(self, elif self.charge_template.data.chargemode.selected == "pv_charging": required_current, submode, message, phases = self.charge_template.pv_charging( self.data.get.soc, control_parameter.min_current, charging_type) - elif self.charge_template.data.chargemode.selected == "standby": - # Text von Zeit-und Zielladen nicht überschreiben. - if message is None: - required_current, submode, message = self.charge_template.standby() - else: - required_current, submode, _ = self.charge_template.standby() elif self.charge_template.data.chargemode.selected == "stop": required_current, submode, message = self.charge_template.stop() - if submode == "stop" or submode == "standby" or (self.charge_template.data.chargemode.selected == "stop"): + phases = control_parameter.phases or max_phases_hw + if submode == "stop" or (self.charge_template.data.chargemode.selected == "stop"): state = False return state, message, submode, required_current, phases except Exception as e: From 91a7ebb0347b061c3970df0569a68fa6b95aa74c Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 17 Jan 2025 14:59:06 +0100 Subject: [PATCH 08/59] fix --- packages/control/ev/charge_template.py | 14 ++++++++------ packages/control/ev/ev.py | 3 +++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 58b95eb0bf..4e97204b19 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -289,13 +289,15 @@ def eco_charging(self, current = 0, sub_mode = "stop" message = self.AMOUNT_REACHED - elif (data.data.optional_data.et_provider_available() and - data.data.optional_data.et_price_lower_than_limit(eco_charging.max_price)): - current = min_current - message = self.CHARGING_PRICE_EXCEEDED + elif data.data.optional_data.et_provider_available(): + if data.data.optional_data.et_price_lower_than_limit(eco_charging.max_price): + current = min_current + message = self.CHARGING_PRICE_EXCEEDED + else: + sub_mode = "instant_charging" + message = self.CHARGING_PRICE_LOW else: - sub_mode = "instant_charging" - message = self.CHARGING_PRICE_LOW + current = min_current return current, sub_mode, message, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index b1d4f156cd..8d8a3767ce 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -213,6 +213,9 @@ def get_required_current(self, elif self.charge_template.data.chargemode.selected == "pv_charging": required_current, submode, message, phases = self.charge_template.pv_charging( self.data.get.soc, control_parameter.min_current, charging_type) + elif self.charge_template.data.chargemode.selected == "eco_charging": + required_current, submode, message, phases = self.charge_template.eco_charging( + self.data.get.soc, control_parameter.min_current, charging_type) elif self.charge_template.data.chargemode.selected == "stop": required_current, submode, message = self.charge_template.stop() phases = control_parameter.phases or max_phases_hw From dcae2781f23fdc9df237397139e38f39df6791d7 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 17 Jan 2025 16:25:17 +0100 Subject: [PATCH 09/59] update tests --- packages/control/algorithm/chargemodes.py | 6 +- .../integration_test/pv_charging_test.py | 8 +-- packages/control/chargepoint/chargepoint.py | 8 +-- .../control/chargepoint/get_phases_test.py | 7 +-- packages/control/counter.py | 10 ++-- packages/control/ev/charge_template.py | 17 +++--- packages/control/ev/charge_template_test.py | 60 ++++++++++--------- packages/control/ev/ev.py | 3 + packages/control/general.py | 20 ------- .../data_migration/data_migration.py | 2 +- 10 files changed, 62 insertions(+), 79 deletions(-) diff --git a/packages/control/algorithm/chargemodes.py b/packages/control/algorithm/chargemodes.py index 4ea481c6a4..76bc9906a6 100644 --- a/packages/control/algorithm/chargemodes.py +++ b/packages/control/algorithm/chargemodes.py @@ -21,8 +21,8 @@ (None, Chargemode.STOP, True), (None, Chargemode.STOP, False)) -CONSIDERED_CHARGE_MODES_SURPLUS = CHARGEMODES[0:2] + CHARGEMODES[6:14] -CONSIDERED_CHARGE_MODES_PV_ONLY = CHARGEMODES[10:14] +CONSIDERED_CHARGE_MODES_SURPLUS = CHARGEMODES[0:2] + CHARGEMODES[6:16] +CONSIDERED_CHARGE_MODES_PV_ONLY = CHARGEMODES[10:16] CONSIDERED_CHARGE_MODES_ADDITIONAL_CURRENT = CHARGEMODES[0:10] CONSIDERED_CHARGE_MODES_MIN_CURRENT = CHARGEMODES[0:-1] -CONSIDERED_CHARGE_MODES_NO_CURRENT = CHARGEMODES[14:16] +CONSIDERED_CHARGE_MODES_NO_CURRENT = CHARGEMODES[16:18] diff --git a/packages/control/algorithm/integration_test/pv_charging_test.py b/packages/control/algorithm/integration_test/pv_charging_test.py index 2b7383db3b..c2999f8f19 100644 --- a/packages/control/algorithm/integration_test/pv_charging_test.py +++ b/packages/control/algorithm/integration_test/pv_charging_test.py @@ -7,7 +7,6 @@ from control.chargemode import Chargemode from control import data, loadmanagement from control.algorithm.algorithm import Algorithm -from control.algorithm.algorithm import data as algorithm_data from control.chargepoint.chargepoint_template import CpTemplate from control.chargepoint.chargepoint_state import ChargepointState from dataclass_utils.factories import currents_list_factory @@ -224,6 +223,9 @@ def test_surplus(params: ParamsSurplus, all_cp_pv_charging_3p, all_cp_charging_3 data.data.counter_data["counter6"].data.set.raw_currents_left = params.raw_currents_left_counter6 mockget_component_name_by_id = Mock(return_value="Garage") monkeypatch.setattr(loadmanagement, "get_component_name_by_id", mockget_component_name_by_id) + data.data.cp_data["cp3"].data.set.charging_ev_data.charge_template.data.chargemode.pv_charging.phases_to_use = 1 + data.data.cp_data["cp4"].data.set.charging_ev_data.charge_template.data.chargemode.pv_charging.phases_to_use = 1 + data.data.cp_data["cp5"].data.set.charging_ev_data.charge_template.data.chargemode.pv_charging.phases_to_use = 1 # execution Algorithm().calc_current() @@ -270,8 +272,6 @@ def test_phase_switch(all_cp_pv_charging_3p, all_cp_charging_3p, monkeypatch): data.data.counter_data["counter0"].data.set.raw_power_left = cases_phase_switch[0].raw_power_left data.data.counter_data["counter0"].data.set.raw_currents_left = cases_phase_switch[0].raw_currents_left_counter0 data.data.counter_data["counter6"].data.set.raw_currents_left = cases_phase_switch[0].raw_currents_left_counter6 - mockget_get_phases_chargemode = Mock(return_value=0) - monkeypatch.setattr(algorithm_data.data.general_data, "get_phases_chargemode", mockget_get_phases_chargemode) data.data.cp_data[ "cp3"].data.control_parameter.state = ChargepointState.CHARGING_ALLOWED data.data.cp_data[ @@ -293,8 +293,6 @@ def test_phase_switch_1p_3p(all_cp_pv_charging_1p, monkeypatch): data.data.counter_data["counter0"].data.set.raw_power_left = cases_phase_switch[1].raw_power_left data.data.counter_data["counter0"].data.set.raw_currents_left = cases_phase_switch[1].raw_currents_left_counter0 data.data.counter_data["counter6"].data.set.raw_currents_left = cases_phase_switch[1].raw_currents_left_counter6 - mockget_get_phases_chargemode = Mock(return_value=0) - monkeypatch.setattr(algorithm_data.data.general_data, "get_phases_chargemode", mockget_get_phases_chargemode) data.data.cp_data["cp3"].data.get.currents = [32, 0, 0] data.data.cp_data["cp3"].data.get.power = 7360 data.data.cp_data["cp3"].data.control_parameter.timestamp_last_phase_switch = 1652682252 diff --git a/packages/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index 88859e45bd..321c9076ea 100644 --- a/packages/control/chargepoint/chargepoint.py +++ b/packages/control/chargepoint/chargepoint.py @@ -794,14 +794,12 @@ def _pub_connected_vehicle(self, vehicle: Ev): def cp_ev_chargemode_support_phase_switch(self) -> bool: control_parameter = self.data.control_parameter pv_auto_switch = (control_parameter.chargemode == Chargemode.PV_CHARGING and - data.data.general_data.get_phases_chargemode( - Chargemode.PV_CHARGING.value, - control_parameter.submode) == 0) + self.data.set.charging_ev_data.charge_template.data.chargemode.pv_charging.phases_to_use == 0) scheduled_auto_switch = ( control_parameter.chargemode == Chargemode.SCHEDULED_CHARGING and control_parameter.submode == Chargemode.PV_CHARGING and - data.data.general_data.get_phases_chargemode(Chargemode.SCHEDULED_CHARGING.value, - control_parameter.submode) == 0) + self.data.set.charging_ev_data.charge_template.data.chargemode.scheduled_charging.plans[ + str(self.data.get.connected_vehicle.config.current_plan)].phases_to_use_pv == 0) if ((data.data.general_data.data.chargemode_config.retry_failed_phase_switches and self.data.control_parameter.failed_phase_switches > self.MAX_FAILED_PHASE_SWITCHES) or (data.data.general_data.data.chargemode_config.retry_failed_phase_switches is False and diff --git a/packages/control/chargepoint/get_phases_test.py b/packages/control/chargepoint/get_phases_test.py index 9dd39adead..9c5a981a5d 100644 --- a/packages/control/chargepoint/get_phases_test.py +++ b/packages/control/chargepoint/get_phases_test.py @@ -81,11 +81,8 @@ def __init__(self, @pytest.mark.parametrize("params", cases, ids=[c.name for c in cases]) -def test_get_phases_by_selected_chargemode(monkeypatch, cp: Chargepoint, params: Params): +def test_get_phases_by_selected_chargemode(cp: Chargepoint, params: Params): # setup - mock_chargemode_phases = Mock(name="chargemode_phases", return_value=params.chargemode_phases) - monkeypatch.setattr(data.data.general_data, "get_phases_chargemode", mock_chargemode_phases) - cp.data.config.connected_phases = params.connected_phases cp.data.config.auto_phase_switch_hw = params.auto_phase_switch_hw cp.data.get.charge_state = params.charge_state @@ -98,7 +95,7 @@ def test_get_phases_by_selected_chargemode(monkeypatch, cp: Chargepoint, params: cp.data.control_parameter.phases = params.phases_in_use # execution - phases = cp.get_phases_by_selected_chargemode() + phases = cp.get_phases_by_selected_chargemode(params.chargemode_phases) # evaluation assert phases == params.expected_phases diff --git a/packages/control/counter.py b/packages/control/counter.py index 80531f12a9..37d8b9b4fa 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -7,7 +7,6 @@ from typing import List, Optional, Tuple from control import data -from control.chargemode import Chargemode from control.ev.ev import Ev from control.chargepoint.chargepoint import Chargepoint from control.chargepoint.chargepoint_state import ChargepointState @@ -325,6 +324,7 @@ def switch_on_timer_expired(self, chargepoint: Chargepoint) -> None: msg = None pv_config = data.data.general_data.data.chargemode_config.pv_charging control_parameter = chargepoint.data.control_parameter + charging_ev_data = chargepoint.data.set.charging_ev_data # Timer ist noch nicht abgelaufen if timecheck.check_timestamp(control_parameter.timestamp_switch_on_off, pv_config.switch_on_delay): @@ -337,14 +337,14 @@ def switch_on_timer_expired(self, chargepoint: Chargepoint) -> None: msg = self.SWITCH_ON_EXPIRED.format(pv_config.switch_on_threshold) control_parameter.state = ChargepointState.WAIT_FOR_USING_PHASES - if chargepoint.data.set.charging_ev_data.charge_template.data.chargemode.pv_charging.feed_in_limit: + if charging_ev_data.charge_template.data.chargemode.pv_charging.feed_in_limit: feed_in_yield = pv_config.feed_in_yield else: feed_in_yield = 0 - ev_template = chargepoint.data.set.charging_ev_data.ev_template + ev_template = charging_ev_data.ev_template max_phases_power = ev_template.data.min_current * ev_template.data.max_phases * 230 - if (data.data.general_data.get_phases_chargemode(Chargemode.PV_CHARGING.value, - control_parameter.submode) == 0 and + if (control_parameter.submode == "pv_charging" and + charging_ev_data.charge_template.data.chargemode.pv_charging.phases_to_use == 0 and chargepoint.cp_ev_support_phase_switch() and self.get_usable_surplus(feed_in_yield) > max_phases_power): control_parameter.phases = ev_template.data.max_phases diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 4e97204b19..89a0a57a47 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -153,6 +153,7 @@ def time_charging(self, message = None sub_mode = "time_charging" id = None + phases = None try: if self.data.time_charging.plans: plan = timecheck.check_plans_timeframe(self.data.time_charging.plans) @@ -160,12 +161,12 @@ def time_charging(self, current = plan.current if charging_type == ChargingType.AC.value else plan.dc_current phases = plan.phases_to_use id = plan.id - if plan.limit.selected == "soc" and soc and soc > plan.limit.soc: + if plan.limit.selected == "soc" and soc and soc >= plan.limit.soc: # SoC-Limit erreicht current = 0 sub_mode = "stop" message = self.TIME_CHARGING_SOC_REACHED - elif plan.limit.selected == "amount" and used_amount_time_charging > plan.limit.amount: + elif plan.limit.selected == "amount" and used_amount_time_charging >= plan.limit.amount: # Energie-Limit erreicht current = 0 sub_mode = "stop" @@ -203,13 +204,13 @@ def instant_charging(self, else: current = instant_charging.dc_current - if instant_charging.limit.selected == "soc" and soc and soc > instant_charging.limit.soc: + if instant_charging.limit.selected == "soc" and soc and soc >= instant_charging.limit.soc: current = 0 sub_mode = "stop" message = self.SOC_REACHED elif instant_charging.limit.selected == "amount": - if imported_instant_charging > self.data.chargemode.instant_charging.limit.amount: - current = 0, + if imported_instant_charging >= self.data.chargemode.instant_charging.limit.amount: + current = 0 sub_mode = "stop" message = self.AMOUNT_REACHED return current, sub_mode, message, phases @@ -239,7 +240,7 @@ def pv_charging(self, sub_mode = "stop" message = self.SOC_REACHED elif (pv_charging.limit.selected == "amount" and - pv_charging > self.data.chargemode.instant_charging.limit.amount): + pv_charging >= self.data.chargemode.instant_charging.limit.amount): current = 0, sub_mode = "stop" message = self.AMOUNT_REACHED @@ -280,12 +281,12 @@ def eco_charging(self, phases = eco_charging.phases_to_use current = eco_charging.current if charging_type == ChargingType.AC.value else eco_charging.dc_current - if eco_charging.limit.selected == "soc" and soc and soc > eco_charging.limit.soc: + if eco_charging.limit.selected == "soc" and soc and soc >= eco_charging.limit.soc: current = 0 sub_mode = "stop" message = self.SOC_REACHED elif (eco_charging.limit.selected == "amount" and - eco_charging > self.data.chargemode.instant_charging.limit.amount): + eco_charging >= self.data.chargemode.instant_charging.limit.amount): current = 0, sub_mode = "stop" message = self.AMOUNT_REACHED diff --git a/packages/control/ev/charge_template_test.py b/packages/control/ev/charge_template_test.py index 8439181b6d..3d2c190d1e 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -25,32 +25,32 @@ def data_module() -> None: @pytest.mark.parametrize( "plans, soc, used_amount_time_charging, plan_found, expected", [ - pytest.param({}, 0, 0, None, (0, "stop", ChargeTemplate.TIME_CHARGING_NO_PLAN_CONFIGURED, None), + pytest.param({}, 0, 0, None, (0, "stop", ChargeTemplate.TIME_CHARGING_NO_PLAN_CONFIGURED, None, None), id="no plan defined"), pytest.param({"0": TimeChargingPlan(id=0)}, 0, 0, None, - (0, "stop", ChargeTemplate.TIME_CHARGING_NO_PLAN_ACTIVE, None), id="no plan active"), + (0, "stop", ChargeTemplate.TIME_CHARGING_NO_PLAN_ACTIVE, None, None), id="no plan active"), pytest.param({"0": TimeChargingPlan(id=0)}, 0, 0, TimeChargingPlan(id=0), - (16, "time_charging", None, 0), id="plan active"), + (16, "time_charging", None, 0, 1), id="plan active"), pytest.param({"0": TimeChargingPlan(id=0, limit=Limit(selected="soc"))}, 100, 0, TimeChargingPlan(id=0, limit=Limit(selected="soc")), - (0, "stop", ChargeTemplate.TIME_CHARGING_SOC_REACHED, 0), + (0, "stop", ChargeTemplate.TIME_CHARGING_SOC_REACHED, 0, 1), id="plan active, soc is reached"), pytest.param({"0": TimeChargingPlan(id=0, limit=Limit(selected="soc"))}, 40, 0, TimeChargingPlan(id=0, limit=Limit(selected="soc")), - (16, "time_charging", None, 0), id="plan active, soc is not reached"), + (16, "time_charging", None, 0, 1), id="plan active, soc is not reached"), pytest.param({"0": TimeChargingPlan(id=0, limit=Limit(selected="soc"))}, None, 0, TimeChargingPlan(id=0, limit=Limit(selected="soc")), - (16, "time_charging", None, 0), id="plan active, soc is not defined"), + (16, "time_charging", None, 0, 1), id="plan active, soc is not defined"), pytest.param({"0": TimeChargingPlan(id=0, limit=Limit(selected="amount"))}, 0, 1500, TimeChargingPlan(id=0, limit=Limit(selected="amount")), - (0, "stop", ChargeTemplate.TIME_CHARGING_AMOUNT_REACHED, 0), + (0, "stop", ChargeTemplate.TIME_CHARGING_AMOUNT_REACHED, 0, 1), id="plan active, used_amount_time_charging is reached"), pytest.param({"0": TimeChargingPlan(id=0, limit=Limit(selected="amount"))}, 0, 500, TimeChargingPlan(id=0, limit=Limit(selected="amount")), - (16, "time_charging", None, 0), + (16, "time_charging", None, 0, 1), id="plan active, used_amount_time_charging is not reached"), pytest.param({"0": TimeChargingPlan(id=0)}, 0, 0, None, - (0, "stop", ChargeTemplate.TIME_CHARGING_NO_PLAN_ACTIVE, None), id="plan defined but not found"), + (0, "stop", ChargeTemplate.TIME_CHARGING_NO_PLAN_ACTIVE, None, None), id="plan defined but not found"), ] ) def test_time_charging(plans: Dict[int, TimeChargingPlan], soc: float, used_amount_time_charging: float, @@ -73,19 +73,18 @@ def test_time_charging(plans: Dict[int, TimeChargingPlan], soc: float, used_amou @pytest.mark.parametrize( "selected, current_soc, used_amount, expected", [ - pytest.param("none", 0, 0, (10, "instant_charging", None), id="without limit"), - pytest.param("soc", None, 0, (10, "instant_charging", None), id="limit soc: soc not defined"), - pytest.param("soc", 49, 0, (10, "instant_charging", None), id="limit soc: soc not reached"), - pytest.param("soc", 50, 0, (0, "stop", ChargeTemplate.SOC_REACHED), + pytest.param("none", 0, 0, (10, "instant_charging", None, 3), id="without limit"), + pytest.param("soc", None, 0, (10, "instant_charging", None, 3), id="limit soc: soc not defined"), + pytest.param("soc", 49, 0, (10, "instant_charging", None, 3), id="limit soc: soc not reached"), + pytest.param("soc", 50, 0, (0, "stop", ChargeTemplate.SOC_REACHED, 3), id="limit soc: soc reached"), - pytest.param("amount", 0, 999, (10, "instant_charging", None), id="limit amount: amount not reached"), - pytest.param("amount", 0, 1000, (0, "stop", ChargeTemplate.AMOUNT_REACHED), + pytest.param("amount", 0, 999, (10, "instant_charging", None, 3), id="limit amount: amount not reached"), + pytest.param("amount", 0, 1000, (0, "stop", ChargeTemplate.AMOUNT_REACHED, 3), id="limit amount: amount reached"), ]) def test_instant_charging(selected: str, current_soc: float, used_amount: float, expected: Tuple[int, str, Optional[str]]): # setup - data.data.optional_data.data.et.active = False ct = ChargeTemplate(0) ct.data.chargemode.instant_charging.limit.selected = selected @@ -99,20 +98,24 @@ def test_instant_charging(selected: str, current_soc: float, used_amount: float, @pytest.mark.parametrize( "min_soc, min_current, current_soc, expected", [ - pytest.param(0, 0, 100, (0, "stop", ChargeTemplate.PV_CHARGING_SOC_REACHED), id="max soc reached"), - pytest.param(15, 0, 14, (10, "instant_charging", ChargeTemplate.PV_CHARGING_SOC_CHARGING), + pytest.param(0, 0, 100, (0, "stop", ChargeTemplate.SOC_REACHED, 0), id="max soc reached"), + pytest.param(15, 0, 14, (10, "instant_charging", ChargeTemplate.PV_CHARGING_SOC_CHARGING, 3), id="min soc not reached"), - pytest.param(15, 0, None, (6, "pv_charging", None), id="soc not defined"), - pytest.param(15, 8, 15, (8, "instant_charging", ChargeTemplate.PV_CHARGING_MIN_CURRENT_CHARGING), + pytest.param(15, 0, None, (6, "pv_charging", None, 0), id="soc not defined"), + pytest.param(15, 8, 15, (8, "instant_charging", ChargeTemplate.PV_CHARGING_MIN_CURRENT_CHARGING, 0), id="min current configured"), - pytest.param(15, 0, 15, (6, "pv_charging", None), id="bare pv charging"), + pytest.param(15, 0, 15, (6, "pv_charging", None, 0), id="bare pv charging"), ]) def test_pv_charging(min_soc: int, min_current: int, current_soc: float, - expected: Tuple[int, str, Optional[str]]): + expected: Tuple[int, str, Optional[str], int]): # setup ct = ChargeTemplate(0) ct.data.chargemode.pv_charging.min_soc = min_soc ct.data.chargemode.pv_charging.min_current = min_current + ct.data.chargemode.pv_charging.phases_to_use = 0 + ct.data.chargemode.pv_charging.phases_to_use_min_soc = 3 + ct.data.chargemode.pv_charging.limit.selected = "soc" + ct.data.chargemode.pv_charging.limit.soc = 90 data.data.bat_all_data.data.config.configured = True # execution @@ -225,7 +228,7 @@ def test_search_plan(check_duration_return1: Tuple[Optional[float], bool], pytest.param(SelectedPlan(duration=3600), 90, 0, "soc", (0, "stop", ChargeTemplate.SCHEDULED_CHARGING_REACHED_LIMIT_SOC, 1), id="reached limit soc"), pytest.param(SelectedPlan(duration=3600), 80, 0, "soc", (6, "pv_charging", - ChargeTemplate.SCHEDULED_CHARGING_REACHED_SCHEDULED_SOC, 3), id="reached scheduled soc"), + ChargeTemplate.SCHEDULED_CHARGING_REACHED_SCHEDULED_SOC, 0), id="reached scheduled soc"), pytest.param(SelectedPlan(phases=3, duration=3600), 0, 1000, "amount", (0, "stop", ChargeTemplate.SCHEDULED_CHARGING_REACHED_AMOUNT, 3), id="reached amount"), pytest.param(SelectedPlan(remaining_time=299, duration=3600), 0, 999, "amount", @@ -245,7 +248,7 @@ def test_search_plan(check_duration_return1: Tuple[Optional[float], bool], ChargeTemplate.SCHEDULED_CHARGING_MAX_CURRENT.format(16), 3), id="few minutes too late, but didn't miss for today"), pytest.param(SelectedPlan(remaining_time=301, duration=3600), 79, 0, "soc", - (6, "pv_charging", ChargeTemplate.SCHEDULED_CHARGING_USE_PV.format("um 8:45 Uhr"), 3), + (6, "pv_charging", ChargeTemplate.SCHEDULED_CHARGING_USE_PV.format("um 8:45 Uhr"), 0), id="too early, use pv"), ]) def test_scheduled_charging_calc_current(plan_data: SelectedPlan, @@ -259,6 +262,8 @@ def test_scheduled_charging_calc_current(plan_data: SelectedPlan, plan.limit.selected = selected # json verwandelt Keys in strings ct.data.chargemode.scheduled_charging.plans = {"0": plan} + if plan_data: + plan_data.plan = plan # execution ret = ct.scheduled_charging_calc_current(plan_data, soc, used_amount, 3, 6, 0) @@ -284,7 +289,7 @@ def test_scheduled_charging_calc_current_no_plans(): pytest.param(True, (14, "instant_charging", ChargeTemplate.SCHEDULED_CHARGING_CHEAP_HOUR.format( "Geladen wird zu folgenden Uhrzeiten: 8:00."), 3)), pytest.param(False, (6, "pv_charging", ChargeTemplate.SCHEDULED_CHARGING_EXPENSIVE_HOUR.format( - "Geladen wird zu folgenden Uhrzeiten: 8:00."), 3)), + "Geladen wird zu folgenden Uhrzeiten: 8:00."), 0)), ]) def test_scheduled_charging_calc_current_electricity_tariff(loading_hour, expected, monkeypatch): # setup @@ -292,7 +297,7 @@ def test_scheduled_charging_calc_current_electricity_tariff(loading_hour, expect plan = ScheduledChargingPlan(active=True) plan.limit.selected = "soc" ct.data.chargemode.scheduled_charging.plans = {"0": plan} - ct.data.et.active = True + ct.data.chargemode.scheduled_charging.plans["0"].et_active = True # für Github-Test keinen Zeitstempel verwenden mock_et_get_loading_hours = Mock(return_value=[datetime.datetime( year=2022, month=5, day=16, hour=8, minute=0).timestamp()]) @@ -303,7 +308,8 @@ def test_scheduled_charging_calc_current_electricity_tariff(loading_hour, expect monkeypatch.setattr(timecheck, "is_list_valid", mock_is_list_valid) # execution - ret = ct.scheduled_charging_calc_current(SelectedPlan(remaining_time=301, phases=3, duration=3600), 79, 0, 3, 6, 0) + ret = ct.scheduled_charging_calc_current(SelectedPlan( + plan=plan, remaining_time=301, phases=3, duration=3600), 79, 0, 3, 6, 0) # evaluation assert ret == expected diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 8d8a3767ce..eb12314808 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -221,6 +221,9 @@ def get_required_current(self, phases = control_parameter.phases or max_phases_hw if submode == "stop" or (self.charge_template.data.chargemode.selected == "stop"): state = False + if phases is None: + log.debug("Keine Phasenvorgabe durch Lademodus. Behalte Phasenzahl bei.") + phases = control_parameter.phases return state, message, submode, required_current, phases except Exception as e: log.exception("Fehler im ev-Modul "+str(self.num)) diff --git a/packages/control/general.py b/packages/control/general.py index bb6621828e..2904b9eaa0 100644 --- a/packages/control/general.py +++ b/packages/control/general.py @@ -7,7 +7,6 @@ from control import data from control.bat_all import BatConsiderationMode -from control.chargemode import Chargemode from helpermodules import timecheck log = logging.getLogger(__name__) @@ -112,25 +111,6 @@ class General: def __init__(self): self.data: GeneralData = GeneralData() - def get_phases_chargemode(self, chargemode: str, submode: str) -> Optional[int]: - """ gibt die Anzahl Phasen zurück, mit denen im jeweiligen Lademodus geladen wird. - Wenn der Lademodus Stop oder Standby ist, wird 0 zurückgegeben, da in diesem Fall - die bisher genutzte Phasenzahl weiter genutzt wird, bis der Algorithmus eine Umschaltung vorgibt. - """ - try: - if chargemode == "stop" or chargemode == "standby": - # bei diesen Lademodi kann die bisherige Phasenzahl beibehalten werden. - return None - elif chargemode == "scheduled_charging" and (submode == "pv_charging" or submode == Chargemode.PV_CHARGING): - # todo Lademodus von String auf Enum umstellen - # Phasenumschaltung bei PV-Ueberschuss nutzen - return getattr(self.data.chargemode_config, chargemode).phases_to_use_pv - else: - return getattr(self.data.chargemode_config, chargemode).phases_to_use - except Exception: - log.exception("Fehler im General-Modul") - return 1 - def grid_protection(self): """ Wenn der Netzschutz konfiguriert ist, wird geprüft, ob die Frequenz außerhalb des Normalbereichs liegt und dann der Netzschutz aktiviert. Bei der Ermittlung des benötigten Stroms im EV-Modul wird geprüft, ob diff --git a/packages/helpermodules/data_migration/data_migration.py b/packages/helpermodules/data_migration/data_migration.py index ad7e2c4129..18bffce836 100644 --- a/packages/helpermodules/data_migration/data_migration.py +++ b/packages/helpermodules/data_migration/data_migration.py @@ -206,7 +206,7 @@ def conv_1_9_datetimes(datetime_str): elif row[7] == "3": chargemode = "stop" elif row[7] == "4": - chargemode = "standby" + chargemode = "eco_charging" elif row[7] == "7": chargemode = "scheduled_charging" else: From 63db164a0105ae8edb37267aefd7ea0ea2e6f400 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Mon, 20 Jan 2025 10:15:09 +0100 Subject: [PATCH 10/59] refactor scheduled charging --- packages/control/chargepoint/chargepoint.py | 3 +- packages/control/ev/charge_template.py | 137 ++++++++------------ packages/control/ev/ev.py | 6 +- packages/helpermodules/timecheck.py | 79 ++++------- packages/helpermodules/timecheck_test.py | 34 ----- 5 files changed, 84 insertions(+), 175 deletions(-) diff --git a/packages/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index 321c9076ea..ea78656e95 100644 --- a/packages/control/chargepoint/chargepoint.py +++ b/packages/control/chargepoint/chargepoint.py @@ -632,7 +632,8 @@ def update(self, ev_list: Dict[str, Ev]) -> None: self.data.get.imported, max_phase_hw, self.cp_ev_support_phase_switch(), - self.template.data.charging_type) + self.template.data.charging_type, + self.data.set.log.timestamp_start_charging) phases = self.set_phases(phases) self._pub_connected_vehicle(charging_ev) required_current = self.chargepoint_module.add_conversion_loss_to_current(required_current) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 89a0a57a47..febe5723cc 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -119,9 +119,7 @@ def charge_template_data_factory() -> ChargeTemplateData: @dataclass class SelectedPlan: remaining_time: float = 0 - available_current: float = 14 duration: float = 0 - max_current: int = 16 missing_amount: float = 0 phases: int = 1 plan: Optional[ScheduledChargingPlan] = None @@ -136,7 +134,8 @@ class ChargeTemplate: "topic": ""}) BUFFER = -1200 # nach mehr als 20 Min Überschreitung wird der Termin als verpasst angesehen - CHARGING_PRICE_EXCEEDED = "Keine Ladung, da der aktuelle Strompreis über dem maximalen Strompreis liegt. Falls vorhanden wird mit EVU-Überschuss geladen." + CHARGING_PRICE_EXCEEDED = ("Keine Ladung, da der aktuelle Strompreis über dem maximalen Strompreis liegt. " + + "Falls vorhanden wird mit EVU-Überschuss geladen.") CHARGING_PRICE_LOW = "Ladung, da der aktuelle Strompreis unter dem maximalen Strompreis liegt." TIME_CHARGING_NO_PLAN_CONFIGURED = "Keine Ladung, da keine Zeitfenster für Zeitladen konfiguriert sind." @@ -309,93 +308,60 @@ def scheduled_charging_recent_plan(self, ev_template: EvTemplate, phases: int, used_amount: float, - max_hw_phasess: int, + max_hw_phases: int, phase_switch_supported: bool, - charging_type: str) -> Optional[SelectedPlan]: - """ prüft, ob der Ziel-SoC oder die Ziel-Energiemenge erreicht wurde und stellt den zur Erreichung nötigen - Ladestrom ein. Um etwas mehr Puffer zu haben, wird bis 20 Min nach dem Zieltermin noch geladen, wenn dieser - nicht eingehalten werden konnte. - """ - if phase_switch_supported: - if charging_type == ChargingType.AC.value: - max_current = ev_template.data.max_current_multi_phases - else: - max_current = ev_template.data.dc_max_current - selected_plan = self._search_plan(max_current, soc, ev_template, min( - 3 if plan.phases_to_use == 0 else plan.phases_to_use, max_hw_phases), used_amount, charging_type) - if (selected_plan and - charging_type == ChargingType.AC.value and - selected_plan.plan.phases_to_use == 0 and - selected_plan.remaining_time > 300 and - selected_plan.plan.et_active is False): - max_current = ev_template.data.max_current_single_phase - plan_data_single_phase = self._search_plan( - max_current, soc, ev_template, 1, used_amount, charging_type) - if plan_data_single_phase: - if plan_data_single_phase.remaining_time > 0: - selected_plan = plan_data_single_phase - else: - if charging_type == ChargingType.AC.value: - if phases == 1: - max_current = ev_template.data.max_current_single_phase - else: - max_current = ev_template.data.max_current_multi_phases - else: - max_current = ev_template.data.dc_max_current - selected_plan = self._search_plan(max_current, soc, ev_template, phases, used_amount, charging_type) - return selected_plan - - def _search_plan(self, - max_current: int, - soc: Optional[float], - ev_template: EvTemplate, - max_hw_phases: int, - used_amount: float, - charging_type: str) -> Optional[SelectedPlan]: - smallest_remaining_time = float("inf") - missed_date_today_of_plan_with_smallest_remaining_time = False - selected_plan: Optional[SelectedPlan] = None - battery_capacity = ev_template.data.battery_capacity - for plan in self.data.chargemode.scheduled_charging.plans.values(): - if plan.active: - if plan.limit.selected == "soc" and soc is None: + charging_type: str, + chargemde_switch_timestamp: float) -> Optional[SelectedPlan]: + plans_diff_end_date = [] + for p in self.data.chargemode.scheduled_charging.plans.values(): + if p.active: + if p.limit.selected == "soc" and soc is None: raise ValueError("Um Zielladen mit SoC-Ziel nutzen zu können, bitte ein SoC-Modul konfigurieren " - f"oder im Plan {plan.name} als Begrenzung Energie einstellen.") + f"oder im Plan {p.name} als Begrenzung Energie einstellen.") try: - phases = min(3 if plan.phases_to_use == 0 else plan.phases_to_use, max_hw_phases) - duration, missing_amount = self._calculate_duration( - plan, soc, battery_capacity, used_amount, phases, charging_type, ev_template) - remaining_time, missed_date_today = timecheck.check_duration(plan, duration, self.BUFFER) - if remaining_time: - # Wenn der Zeitpunkt vorüber, aber noch nicht abgelaufen ist oder - # wenn noch gar kein Plan vorhanden ist, - if ((remaining_time < 0 and missed_date_today is False) or - # oder der Zeitpunkt noch nicht vorüber ist - remaining_time > 0): - # Wenn die verbleibende Zeit geringer als die niedrigste bisherige verbleibende Zeit ist - if (remaining_time < smallest_remaining_time or - # oder wenn der Zeitpunkt abgelaufen ist und es noch einen Zeitpunkt gibt, der in - # der Zukunft liegt. - (missed_date_today_of_plan_with_smallest_remaining_time and 0 < remaining_time)): - smallest_remaining_time = remaining_time - missed_date_today_of_plan_with_smallest_remaining_time = missed_date_today - if charging_type == ChargingType.AC.value: - available_current = plan.current - else: - available_current = plan.dc_current - selected_plan = SelectedPlan( - remaining_time=remaining_time, - available_current=available_current, - max_current=max_current, - phases=phases, - plan=plan, - missing_amount=missing_amount, - duration=duration) - log.debug(f"Plan-Nr. {plan.id}: Differenz zum Start {remaining_time}s, Dauer {duration/3600}h, " - f"Termin heute verpasst: {missed_date_today}") + plans_diff_end_date.update( + {p.id: timecheck.check_end_time(p.time, chargemde_switch_timestamp)}) + except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) - return selected_plan + if plans_diff_end_date: + # ermittle den Key vom kleinsten value in plans_diff_end_date + plan_dict = min(plans_diff_end_date, key=lambda x: x.get( + 'plans_diff_end_date', float('inf'))) + plan_id = list(plan_dict.keys())[0] + plan_end_time = list(plan_dict.values())[0] + + plan = self.data.chargemode.scheduled_charging.plans[plan_id] + + if plan.phases_to_use == 0: + if max_hw_phases == 1 or phase_switch_supported is False: + duration, missing_amount = self._calculate_duration( + plan, soc, ev_template.data.battery_capacity, used_amount, 1, charging_type, ev_template) + else: + duration_3p, missing_amount = self._calculate_duration( + plan, soc, ev_template.data.battery_capacity, used_amount, 3, charging_type, ev_template) + duration_1p, missing_amount = self._calculate_duration( + plan, soc, ev_template.data.battery_capacity, used_amount, 1, charging_type, ev_template) + if duration_1p < 0: + # Zeit reicht nicht mehr für einphasiges Laden + duration = duration_3p + phases = 3 + else: + duration = duration_1p + phases = 1 + elif plan.phases_to_use == 3 or plan.phases_to_use == 1: + duration, missing_amount = self._calculate_duration( + plan, soc, ev_template.data.battery_capacity, + used_amount, plan.phases_to_use, charging_type, ev_template) + + start_time = plan_end_time - duration + remaining_time = timecheck.create_timestamp() - start_time + + return SelectedPlan(remaining_time=remaining_time, + duration=duration, + missing_amount=missing_amount, + phases=phases, + plan=plan) def _calculate_duration(self, plan: ScheduledChargingPlan, @@ -405,6 +371,7 @@ def _calculate_duration(self, phases: int, charging_type: str, ev_template: EvTemplate) -> Tuple[float, float]: + if plan.limit.selected == "soc": if soc is not None: missing_amount = ((plan.limit.soc_scheduled - soc) / 100) * battery_capacity diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index eb12314808..1eb0001bab 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -119,7 +119,8 @@ def get_required_current(self, imported: float, max_phases_hw: int, phase_switch_supported: bool, - charging_type: str) -> Tuple[bool, Optional[str], str, float, int]: + charging_type: str, + chargemde_switch_timestamp: float) -> Tuple[bool, Optional[str], str, float, int]: """ ermittelt, ob und mit welchem Strom das EV geladen werden soll (unabhängig vom Lastmanagement) Parameter @@ -154,7 +155,8 @@ def get_required_current(self, used_amount, max_phases_hw, phase_switch_supported, - charging_type) + charging_type, + chargemde_switch_timestamp) soc_request_interval_offset = 0 if plan_data: # Wenn mit einem neuen Plan geladen wird, muss auch die Energiemenge von neuem gezählt werden. diff --git a/packages/helpermodules/timecheck.py b/packages/helpermodules/timecheck.py index 02b116dec1..e04ff78e30 100644 --- a/packages/helpermodules/timecheck.py +++ b/packages/helpermodules/timecheck.py @@ -1,6 +1,5 @@ """prüft, ob Zeitfenster aktuell sind """ -import copy import logging import datetime from typing import Dict, List, Optional, Tuple, TypeVar, Union @@ -113,7 +112,7 @@ def is_timeframe_valid(now: datetime.datetime, begin: datetime.datetime, end: da return state -def check_duration(plan: ScheduledChargingPlan, duration: float, buffer: int) -> Tuple[Optional[float], bool]: +def check_end_time(plan: ScheduledChargingPlan, chargemde_switch_timestamp: float) -> Optional[float]: """ prüft, ob der in angegebene Zeitpunkt abzüglich der Dauer jetzt ist. Um etwas Puffer zu haben, werden bei Überschreiten des Zeitpunkts die nachfolgenden 20 Min auch noch als Ladezeit zurückgegeben. @@ -130,64 +129,38 @@ def check_duration(plan: ScheduledChargingPlan, duration: float, buffer: int) -> if plan.frequency.selected == "once": endDate = datetime.datetime.strptime(plan.frequency.once, "%Y-%m-%d") end = end.replace(endDate.year, endDate.month, endDate.day) - remaining_time = _get_remaining_time(now, duration, end) + remaining_time = end - now elif plan.frequency.selected == "daily": end = end.replace(now.year, now.month, now.day) - remaining_time_today = _get_remaining_time(now, duration, end) - remaining_time, end = check_following_days(now, duration, end, remaining_time_today, buffer) + remaining_time = end - now + if remaining_time.total_seconds() < 0 and end < chargemde_switch_timestamp: + # Wenn auf Zielladen umgeschaltet wurde und der Termin noch nicht vorbei war, noch auf diesen Termin laden. + end = end + datetime.timedelta(days=1) + remaining_time = end - now elif plan.frequency.selected == "weekly": - end = end.replace(now.year, now.month, now.day) - if plan.frequency.weekly[now.weekday()]: - remaining_time = _get_remaining_time(now, duration, end) - # prüfen, ob für den nächsten Tag ein Termin ansteht und heute schon begonnen werden muss if not any(plan.frequency.weekly): raise ValueError("Es muss mindestens ein Tag ausgewählt werden.") - num_of_following_days = _get_next_charging_day(copy.deepcopy(plan.frequency.weekly), now.weekday()) - remaining_time, end = check_following_days(now, duration, end, remaining_time, buffer, num_of_following_days) + end = end.replace(now.year, now.month, now.day + _get_next_charging_day(plan.frequency.weekly, now.weekday())) + remaining_time = end - now else: raise TypeError(f'Unbekannte Häufigkeit {plan.frequency.selected}') - return remaining_time, _missed_date_today(now, end, buffer) - - -def _get_next_charging_day(weekly_temp: List[bool], weekday: int) -> int: - weekly_temp[weekday] = False - try: - return (weekly_temp[weekday:] + weekly_temp[:weekday]).index(True) - except ValueError: - # Es wird nur am Wochentag von weekday geladen. - return 7 - - -def _missed_date_today(now: datetime.datetime, - end: datetime.datetime, - buffer: float): - return end < now + datetime.timedelta(seconds=buffer) - - -def check_following_days(now: datetime.datetime, - duration: float, - end: datetime.datetime, - remaining_time_today: Optional[float], - buffer: float, - num_of_following_days: int = 1) -> Tuple[Optional[float], datetime.datetime]: - # Zeitpunkt heute darf noch nicht verstrichen sein - if remaining_time_today and not _missed_date_today(now, end, buffer): - return remaining_time_today, end - end = end+datetime.timedelta(days=num_of_following_days) - remaining_time = _get_remaining_time(now, duration, end) - return remaining_time, end - - -def _get_remaining_time(now: datetime.datetime, duration: float, end: datetime.datetime) -> float: - """ Return - ------ - neg: Zeitpunkt vorbei - pos: verbleibende Sekunden - """ - delta = datetime.timedelta(seconds=duration) - start_time = end-delta - log.debug(f"delta {delta} start_time {start_time} end {end} now {now}") - return (start_time-now).total_seconds() + if end < chargemde_switch_timestamp: + # Als auf Zielladen umgeschaltet wurde, war der Termin schon vorbei + remaining_time = None + return remaining_time + + +def _get_next_charging_day(weekly: List[bool], weekday: int) -> int: + count = 0 + for i in range(weekday, len(weekly)): + if weekly[i] is True: + return count + count += 1 + for i in range(0, weekday): + if weekly[i] is True: + return count + count += 1 + return count def is_list_valid(hour_list: List[int]) -> bool: diff --git a/packages/helpermodules/timecheck_test.py b/packages/helpermodules/timecheck_test.py index 0f602383ae..8986a03813 100644 --- a/packages/helpermodules/timecheck_test.py +++ b/packages/helpermodules/timecheck_test.py @@ -20,23 +20,6 @@ def __init__(self, name: str, self.second_time = second_time -@pytest.mark.parametrize("begin_hour, begin_min, end_hour, end_min,expected", - [pytest.param(0, 0, 5, 5, 9300, id="too early"), - pytest.param(8, 18, 10, 35, -780, id="start"), - pytest.param(18, 8, 17, 24, -11640, id="missed date") - ]) -def test_get_remaining_time(begin_hour: int, begin_min: int, end_hour: int, end_min: int, expected: int): - # setup - end = datetime.datetime(2022, 9, 26, end_hour, end_min) - begin = datetime.datetime(2022, 9, 26, begin_hour, begin_min) - - # execution - diff = timecheck._get_remaining_time(begin, 9000, end) - - # evaluation - assert expected == diff - - @pytest.mark.parametrize("time, selected, date, expected", [pytest.param("9:00", "once", "2022-05-16", (-7852.0, False), id="once"), pytest.param("8:00", "once", "2022-05-16", (-11452.0, True), id="once missed date"), @@ -79,23 +62,6 @@ def test_get_next_charging_day(weekday: int, weekly: List[bool], expected_days: assert days == expected_days -@pytest.mark.parametrize("now, end, expected_missed", - [pytest.param(datetime.datetime(2022, 9, 29, 8, 40), - datetime.datetime(2022, 9, 29, 9, 5), False), - pytest.param(datetime.datetime(2022, 9, 29, 8, 40), - datetime.datetime(2022, 9, 29, 8, 39), False), - pytest.param(datetime.datetime(2022, 9, 29, 8, 40), - datetime.datetime(2022, 9, 29, 8, 19), True) - ] - ) -def test_missed_date_today(now: datetime.datetime, end: datetime.datetime, expected_missed: bool): - # setup and execution - missed = timecheck._missed_date_today(now, end, -1200) - - # evaluation - assert missed == expected_missed - - @pytest.mark.parametrize( "plan, now, expected_state", [pytest.param(TimeChargingPlan(active=True, time=["10:00", "12:00"], From 601f0dea8d2ec7c8051d2c6fbeacc8618db2bf69 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 21 Jan 2025 10:14:03 +0100 Subject: [PATCH 11/59] tests --- packages/control/ev/charge_template.py | 48 +++++++++++----- packages/control/ev/charge_template_test.py | 62 ++++++++------------- packages/control/ev/ev.py | 4 +- packages/helpermodules/timecheck.py | 13 +++-- packages/helpermodules/timecheck_test.py | 38 +++++++------ 5 files changed, 89 insertions(+), 76 deletions(-) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index febe5723cc..9406f6b449 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -333,6 +333,24 @@ def scheduled_charging_recent_plan(self, plan = self.data.chargemode.scheduled_charging.plans[plan_id] + remaining_time, missing_amount, phases, duration = self._calc_remaining_time( + plan, plan_end_time, soc, ev_template, used_amount, max_hw_phases, phase_switch_supported, charging_type) + + return SelectedPlan(remaining_time=remaining_time, + duration=duration, + missing_amount=missing_amount, + phases=phases, + plan=plan) + + def _calc_remaining_time(self, + plan: ScheduledChargingPlan, + plan_end_time: float, + soc: Optional[float], + ev_template: EvTemplate, + used_amount: float, + max_hw_phases: int, + phase_switch_supported: bool, + charging_type: str) -> SelectedPlan: if plan.phases_to_use == 0: if max_hw_phases == 1 or phase_switch_supported is False: duration, missing_amount = self._calculate_duration( @@ -355,13 +373,8 @@ def scheduled_charging_recent_plan(self, used_amount, plan.phases_to_use, charging_type, ev_template) start_time = plan_end_time - duration - remaining_time = timecheck.create_timestamp() - start_time - - return SelectedPlan(remaining_time=remaining_time, - duration=duration, - missing_amount=missing_amount, - phases=phases, - plan=plan) + remaining_time = start_time - timecheck.create_timestamp() + return remaining_time, missing_amount, phases, duration def _calculate_duration(self, plan: ScheduledChargingPlan, @@ -413,7 +426,9 @@ def scheduled_charging_calc_current(self, used_amount: float, control_parameter_phases: int, min_current: int, - soc_request_interval_offset: int) -> Tuple[float, str, str, int]: + soc_request_interval_offset: int, + charging_type: str, + ev_template: EvTemplate) -> Tuple[float, str, str, int]: current = 0 submode = "stop" if selected_plan is None: @@ -424,6 +439,12 @@ def scheduled_charging_calc_current(self, plan = selected_plan.plan limit = plan.limit phases = selected_plan.phases + if charging_type == ChargingType.AC.value: + plan_current = plan.current + max_current = ev_template.data.max_current_multi_phases + else: + plan_current = plan.dc_current + max_current = ev_template.data.dc_max_current log.debug("Verwendeter Plan: "+str(plan.name)) if limit.selected == "soc" and soc >= limit.soc_limit and soc >= limit.soc_scheduled: message = self.SCHEDULED_CHARGING_REACHED_LIMIT_SOC @@ -442,18 +463,17 @@ def scheduled_charging_calc_current(self, limit_string = self.SCHEDULED_CHARGING_LIMITED_BY_SOC.format(limit.soc_scheduled) else: limit_string = self.SCHEDULED_CHARGING_LIMITED_BY_AMOUNT.format(limit.amount/1000) - message = self.SCHEDULED_CHARGING_IN_TIME.format( - selected_plan.available_current, limit_string, plan.time) - current = selected_plan.available_current + message = self.SCHEDULED_CHARGING_IN_TIME.format(plan_current, limit_string, plan.time) + current = plan_current submode = "instant_charging" # weniger als die berechnete Zeit verfügbar # Ladestart wurde um maximal 20 Min verpasst. elif selected_plan.remaining_time <= 0 - soc_request_interval_offset: if selected_plan.duration + selected_plan.remaining_time < 0: - current = selected_plan.max_current + current = max_current else: current = min(selected_plan.missing_amount/((selected_plan.duration + selected_plan.remaining_time) / - 3600)/(phases*230), selected_plan.max_current) + 3600)/(phases*230), max_current) message = self.SCHEDULED_CHARGING_MAX_CURRENT.format(round(current, 2)) submode = "instant_charging" else: @@ -469,7 +489,7 @@ def scheduled_charging_calc_current(self, log.debug(f"Günstige Ladezeiten: {hour_list}") if timecheck.is_list_valid(hour_list): message = self.SCHEDULED_CHARGING_CHEAP_HOUR - current = selected_plan.available_current + current = plan_current submode = "instant_charging" elif ((limit.selected == "soc" and soc <= limit.soc_limit) or (limit.selected == "amount" and used_amount < limit.amount)): diff --git a/packages/control/ev/charge_template_test.py b/packages/control/ev/charge_template_test.py index 3d2c190d1e..4678d7e64b 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -125,47 +125,29 @@ def test_pv_charging(min_soc: int, min_current: int, current_soc: float, assert ret == expected -Params = NamedTuple("Params", [("name", str), - ("phase_switch_supported", bool), - ("chargemode_phases", int), - ("search_plan", Optional[SelectedPlan]), - ("expected_max_current", int), - ("phases", int), - ("max_phases", int), - ("expected_phases", int)]) - -cases = [ - Params(name="no phase switch, one phase", phase_switch_supported=False, chargemode_phases=0, - search_plan=None, phases=1, max_phases=3, expected_max_current=32, expected_phases=1), - Params(name="no phase switch, multi phase", phase_switch_supported=False, chargemode_phases=0, - search_plan=None, phases=3, max_phases=3, expected_max_current=16, expected_phases=3), - Params(name="no automatic mode, multi phase", phase_switch_supported=True, chargemode_phases=2, - search_plan=None, phases=2, max_phases=2, expected_max_current=16, expected_phases=2), - Params(name="select phases, not enough time", phase_switch_supported=True, chargemode_phases=0, search_plan=Mock( - spec=SelectedPlan, remaining_time=300), phases=1, max_phases=3, expected_max_current=16, expected_phases=3), - Params(name="select phases, enough time", phase_switch_supported=True, chargemode_phases=0, search_plan=Mock( - spec=SelectedPlan, remaining_time=301), phases=1, max_phases=3, expected_max_current=32, expected_phases=1) -] - - -@pytest.mark.parametrize("params", cases, ids=[c.name for c in cases]) -def test_scheduled_charging_recent_plan(params: Params, monkeypatch): +@pytest.mark.parametrize("phases_to_use, calc_duration, max_hw_phases, phase_switch_supported, expected", + [ + pytest.param(0, (1000, 3), 1, True, (3748, 3, 1, 1000), id="automatic, one hw phase"), + pytest.param(0, (1000, 3), 3, False, (3748, 3, 3, 1000), id="automatic, no phase switch"), + pytest.param(0, (1000, 3), 3, True, (3748, 3, 3, 1000), id="automatic, 3p"), + pytest.param(0, [(5000, 3)], 3, True, (5248, 3, 1, 1000), id="automatic, 1p"), + pytest.param(0, (1000, 3), 3, True, (5248, 3, 3, 1000), id="end time in past"), + ]) +def test_calc_remaining_time(phases_to_use, calc_duration, max_hw_phases, phase_switch_supported, expected, monkeypatch): # setup - ct = ChargeTemplate(0) - get_phases_chargemode_mock = Mock(return_value=params.chargemode_phases) - monkeypatch.setattr(data.data.general_data, "get_phases_chargemode", get_phases_chargemode_mock) - search_plan_mock = Mock(return_value=params.search_plan) - monkeypatch.setattr(ChargeTemplate, "_search_plan", search_plan_mock) - evt_data = Mock(spec=EvTemplateData, max_current_multi_phases=16, max_current_single_phase=32) - evt = Mock(spec=EvTemplate, data=evt_data) + ct = ChargeTemplate() + plan = ScheduledChargingPlan(phases_to_use=phases_to_use) + calculate_duration_mock = Mock(return_value=calc_duration) + monkeypatch.setattr(ChargeTemplate, "_calculate_duration", calculate_duration_mock) + evt = Mock(spec=EvTemplate) # execution - ct.scheduled_charging_recent_plan(50, evt, params.phases, 5, params.max_phases, - params.phase_switch_supported, ChargingType.AC.value) + remaining_time, missing_amount, phases, duration = ct._calc_remaining_time( + plan, 1652688000, 50, evt, 3000, max_hw_phases, phase_switch_supported, ChargingType.AC.value) + # end time 16.5.22 10:00 # evaluation - assert search_plan_mock.call_args.args[0] == params.expected_max_current - assert search_plan_mock.call_args.args[3] == params.expected_phases + assert (remaining_time, missing_amount, phases, duration) == expected @pytest.mark.parametrize( @@ -204,7 +186,7 @@ def test_search_plan(check_duration_return1: Tuple[Optional[float], bool], calculate_duration_mock = Mock(return_value=(100, 200)) monkeypatch.setattr(ChargeTemplate, "_calculate_duration", calculate_duration_mock) check_duration_mock = Mock(side_effect=[check_duration_return1, check_duration_return2]) - monkeypatch.setattr(timecheck, "check_duration", check_duration_mock) + monkeypatch.setattr(timecheck, "check_end_time", check_duration_mock) ct = ChargeTemplate(0) plan_mock_0 = Mock(spec=ScheduledChargingPlan, active=True, current=14, id=0, limit=Limit(selected="amount")) plan_mock_1 = Mock(spec=ScheduledChargingPlan, active=True, current=14, id=1, limit=Limit(selected="amount")) @@ -266,7 +248,7 @@ def test_scheduled_charging_calc_current(plan_data: SelectedPlan, plan_data.plan = plan # execution - ret = ct.scheduled_charging_calc_current(plan_data, soc, used_amount, 3, 6, 0) + ret = ct.scheduled_charging_calc_current(plan_data, soc, used_amount, 3, 6, 0, ChargingType.AC.value, EvTemplate()) # evaluation assert ret == expected @@ -277,7 +259,7 @@ def test_scheduled_charging_calc_current_no_plans(): ct = ChargeTemplate(0) # execution - ret = ct.scheduled_charging_calc_current(None, 63, 5, 3, 6, 0) + ret = ct.scheduled_charging_calc_current(None, 63, 5, 3, 6, 0, ChargingType.AC.value, EvTemplate()) # evaluation assert ret == (0, "stop", ChargeTemplate.SCHEDULED_CHARGING_NO_PLANS_CONFIGURED, 3) @@ -309,7 +291,7 @@ def test_scheduled_charging_calc_current_electricity_tariff(loading_hour, expect # execution ret = ct.scheduled_charging_calc_current(SelectedPlan( - plan=plan, remaining_time=301, phases=3, duration=3600), 79, 0, 3, 6, 0) + plan=plan, remaining_time=301, phases=3, duration=3600), 79, 0, 3, 6, 0, ChargingType.AC.value, EvTemplate()) # evaluation assert ret == expected diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 1eb0001bab..e548b4dae1 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -179,7 +179,9 @@ def get_required_current(self, used_amount, control_parameter.phases, control_parameter.min_current, - soc_request_interval_offset) + soc_request_interval_offset, + charging_type, + self.ev_template) # Wenn Zielladen auf Überschuss wartet, prüfen, ob Zeitladen aktiv ist. if (submode != "instant_charging" and diff --git a/packages/helpermodules/timecheck.py b/packages/helpermodules/timecheck.py index e04ff78e30..30b6079041 100644 --- a/packages/helpermodules/timecheck.py +++ b/packages/helpermodules/timecheck.py @@ -133,7 +133,7 @@ def check_end_time(plan: ScheduledChargingPlan, chargemde_switch_timestamp: floa elif plan.frequency.selected == "daily": end = end.replace(now.year, now.month, now.day) remaining_time = end - now - if remaining_time.total_seconds() < 0 and end < chargemde_switch_timestamp: + if remaining_time.total_seconds() < 0 and end.timestamp() < chargemde_switch_timestamp: # Wenn auf Zielladen umgeschaltet wurde und der Termin noch nicht vorbei war, noch auf diesen Termin laden. end = end + datetime.timedelta(days=1) remaining_time = end - now @@ -142,12 +142,17 @@ def check_end_time(plan: ScheduledChargingPlan, chargemde_switch_timestamp: floa raise ValueError("Es muss mindestens ein Tag ausgewählt werden.") end = end.replace(now.year, now.month, now.day + _get_next_charging_day(plan.frequency.weekly, now.weekday())) remaining_time = end - now + if remaining_time.total_seconds() < 0 and end.timestamp() < chargemde_switch_timestamp: + end = end.replace(now.year, now.month, now.day + + _get_next_charging_day(plan.frequency.weekly, now.weekday()+1)+1) + remaining_time = end - now else: raise TypeError(f'Unbekannte Häufigkeit {plan.frequency.selected}') - if end < chargemde_switch_timestamp: + if end.timestamp() < chargemde_switch_timestamp: # Als auf Zielladen umgeschaltet wurde, war der Termin schon vorbei - remaining_time = None - return remaining_time + return None + else: + return remaining_time.total_seconds() def _get_next_charging_day(weekly: List[bool], weekday: int) -> int: diff --git a/packages/helpermodules/timecheck_test.py b/packages/helpermodules/timecheck_test.py index 8986a03813..9d4f740e5e 100644 --- a/packages/helpermodules/timecheck_test.py +++ b/packages/helpermodules/timecheck_test.py @@ -3,7 +3,6 @@ from unittest.mock import MagicMock, Mock import pytest -from control.ev.charge_template import ChargeTemplate from helpermodules import timecheck from helpermodules.abstract_plans import AutolockPlan, Frequency, ScheduledChargingPlan, TimeChargingPlan @@ -20,37 +19,42 @@ def __init__(self, name: str, self.second_time = second_time -@pytest.mark.parametrize("time, selected, date, expected", - [pytest.param("9:00", "once", "2022-05-16", (-7852.0, False), id="once"), - pytest.param("8:00", "once", "2022-05-16", (-11452.0, True), id="once missed date"), - pytest.param("12:00", "daily", [], (2948.0, False), id="daily today"), - pytest.param("2:00", "daily", [], (53348.0, False), id="daily missed today, use next day"), - pytest.param("8:05", "weekly", [True, False, False, False, - False, False, False], (593648.0, False), id="weekly missed today"), +@pytest.mark.parametrize("time, selected, date, expected_remaining_time", + [pytest.param("9:00", "once", "2022-05-16", 1148, id="once"), + pytest.param("7:55", "once", "2022-05-16", None, id="missed date"), + pytest.param("8:05", "once", "2022-05-16", -2152, id="once missed date"), + pytest.param("12:00", "daily", [], 11948, id="daily today"), + pytest.param("2:00", "daily", [], 62348, id="daily missed today, use next day"), + pytest.param("7:55", "weekly", [True, False, False, False, + False, False, False], 602048, id="weekly missed today"), pytest.param("2:00", "weekly", [False, False, True, False, False, False, False], - (139748.0, False), - id="weekly missed today's date, no date on next day"), + 148748, + id="weekly missed today's date, no date on next day"), pytest.param("2:00", "weekly", [True, True, False, False, False, False, False], - (53348.0, False), - id="weekly missed today's date, date on next day"), + 62348, + id="weekly missed today's date, date on next day"), ] ) -def test_check_duration(time: str, selected: str, date: List, expected: float): +def test_check_end_time(time: str, + selected: str, + date: List, + expected_remaining_time: float): # setup plan = Mock(spec=ScheduledChargingPlan, time=time, frequency=Mock(spec=Frequency, selected=selected,)) if date: setattr(plan.frequency, selected, date) # execution - remaining_time, missed_date_today = timecheck.check_duration(plan, 9000, ChargeTemplate.BUFFER) + remaining_time = timecheck.check_end_time( + plan, chargemde_switch_timestamp=1652680800) # angesteckt am 16.5.22 um 8:00 # evaluation - assert (remaining_time, missed_date_today) == expected + assert remaining_time == expected_remaining_time @pytest.mark.parametrize("weekday, weekly, expected_days", - [pytest.param(0, [True, True, False, False, False, False, False], 1), - pytest.param(0, [True, False, False, False, False, False, False], 7), + [pytest.param(0, [True, True, False, False, False, False, False], 0), + pytest.param(1, [True, False, False, False, False, False, False], 6), pytest.param(3, [True, False, False, False, False, False, False], 4), ] ) From ab0e47b9d04e8c8bc15c7b9f87c2ae6b716493f2 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 21 Jan 2025 16:16:33 +0100 Subject: [PATCH 12/59] test --- packages/control/ev/charge_template.py | 40 ++++++++---- packages/control/ev/charge_template_test.py | 72 ++++++++++++--------- packages/control/ev/ev.py | 3 +- 3 files changed, 69 insertions(+), 46 deletions(-) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 9406f6b449..6626b25695 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -6,6 +6,7 @@ from control import data from control.chargepoint.charging_type import ChargingType +from control.chargepoint.control_parameter import ControlParameter from control.ev.ev_template import EvTemplate from dataclass_utils.factories import empty_dict_factory from helpermodules.abstract_plans import Limit, limit_factory, ScheduledChargingPlan, TimeChargingPlan @@ -311,7 +312,8 @@ def scheduled_charging_recent_plan(self, max_hw_phases: int, phase_switch_supported: bool, charging_type: str, - chargemde_switch_timestamp: float) -> Optional[SelectedPlan]: + chargemde_switch_timestamp: float, + control_parameter: ControlParameter) -> Optional[SelectedPlan]: plans_diff_end_date = [] for p in self.data.chargemode.scheduled_charging.plans.values(): if p.active: @@ -328,19 +330,23 @@ def scheduled_charging_recent_plan(self, # ermittle den Key vom kleinsten value in plans_diff_end_date plan_dict = min(plans_diff_end_date, key=lambda x: x.get( 'plans_diff_end_date', float('inf'))) - plan_id = list(plan_dict.keys())[0] - plan_end_time = list(plan_dict.values())[0] + if plan_dict: + plan_id = list(plan_dict.keys())[0] + plan_end_time = list(plan_dict.values())[0] - plan = self.data.chargemode.scheduled_charging.plans[plan_id] + plan = self.data.chargemode.scheduled_charging.plans[plan_id] - remaining_time, missing_amount, phases, duration = self._calc_remaining_time( - plan, plan_end_time, soc, ev_template, used_amount, max_hw_phases, phase_switch_supported, charging_type) + remaining_time, missing_amount, phases, duration = self._calc_remaining_time( + plan, plan_end_time, soc, ev_template, used_amount, max_hw_phases, phase_switch_supported, + charging_type, control_parameter.phases) - return SelectedPlan(remaining_time=remaining_time, - duration=duration, - missing_amount=missing_amount, - phases=phases, - plan=plan) + return SelectedPlan(remaining_time=remaining_time, + duration=duration, + missing_amount=missing_amount, + phases=phases, + plan=plan) + else: + return None def _calc_remaining_time(self, plan: ScheduledChargingPlan, @@ -350,11 +356,18 @@ def _calc_remaining_time(self, used_amount: float, max_hw_phases: int, phase_switch_supported: bool, - charging_type: str) -> SelectedPlan: + charging_type: str, + control_parameter_phases) -> SelectedPlan: if plan.phases_to_use == 0: - if max_hw_phases == 1 or phase_switch_supported is False: + if max_hw_phases == 1: duration, missing_amount = self._calculate_duration( plan, soc, ev_template.data.battery_capacity, used_amount, 1, charging_type, ev_template) + phases = 1 + elif phase_switch_supported is False: + duration, missing_amount = self._calculate_duration( + plan, soc, ev_template.data.battery_capacity, used_amount, control_parameter_phases, + charging_type, ev_template) + phases = control_parameter_phases else: duration_3p, missing_amount = self._calculate_duration( plan, soc, ev_template.data.battery_capacity, used_amount, 3, charging_type, ev_template) @@ -371,6 +384,7 @@ def _calc_remaining_time(self, duration, missing_amount = self._calculate_duration( plan, soc, ev_template.data.battery_capacity, used_amount, plan.phases_to_use, charging_type, ev_template) + phases = plan.phases_to_use start_time = plan_end_time - duration remaining_time = start_time - timecheck.create_timestamp() diff --git a/packages/control/ev/charge_template_test.py b/packages/control/ev/charge_template_test.py index 4678d7e64b..548d1500ba 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -1,11 +1,12 @@ import datetime -from typing import Dict, NamedTuple, Optional, Tuple +from typing import Dict, Optional, Tuple from unittest.mock import Mock import pytest from control import data from control import optional +from control.chargepoint.control_parameter import ControlParameter from control.ev.charge_template import SelectedPlan from control.chargepoint.charging_type import ChargingType from control.ev.ev import ChargeTemplate @@ -50,7 +51,8 @@ def data_module() -> None: (16, "time_charging", None, 0, 1), id="plan active, used_amount_time_charging is not reached"), pytest.param({"0": TimeChargingPlan(id=0)}, 0, 0, None, - (0, "stop", ChargeTemplate.TIME_CHARGING_NO_PLAN_ACTIVE, None, None), id="plan defined but not found"), + (0, "stop", ChargeTemplate.TIME_CHARGING_NO_PLAN_ACTIVE, None, None), + id="plan defined but not found"), ] ) def test_time_charging(plans: Dict[int, TimeChargingPlan], soc: float, used_amount_time_charging: float, @@ -127,23 +129,29 @@ def test_pv_charging(min_soc: int, min_current: int, current_soc: float, @pytest.mark.parametrize("phases_to_use, calc_duration, max_hw_phases, phase_switch_supported, expected", [ - pytest.param(0, (1000, 3), 1, True, (3748, 3, 1, 1000), id="automatic, one hw phase"), - pytest.param(0, (1000, 3), 3, False, (3748, 3, 3, 1000), id="automatic, no phase switch"), - pytest.param(0, (1000, 3), 3, True, (3748, 3, 3, 1000), id="automatic, 3p"), - pytest.param(0, [(5000, 3)], 3, True, (5248, 3, 1, 1000), id="automatic, 1p"), - pytest.param(0, (1000, 3), 3, True, (5248, 3, 3, 1000), id="end time in past"), + pytest.param(0, [(1000, 3)], 1, True, (3748, 3, 1, 1000), id="automatic, one hw phase"), + pytest.param(0, [(1000, 3)], 3, False, (3748, 3, 2, 1000), + id="automatic, no phase switch"), + pytest.param(0, [(1000, 3), (-50, 3)], 3, True, (3748, 3, 3, 1000), id="automatic, 3p"), + pytest.param(0, [(5000, 3), (1500, 3)], 3, True, (3248, 3, 1, 1500), id="automatic, 1p"), + pytest.param(3, [(5000, 3)], 3, True, (-252, 3, 3, 5000), id="3p"), + pytest.param(1, [(5000, 3)], 3, True, (-252, 3, 1, 5000), id="1p"), ]) -def test_calc_remaining_time(phases_to_use, calc_duration, max_hw_phases, phase_switch_supported, expected, monkeypatch): +def test_calc_remaining_time(phases_to_use, + calc_duration, + max_hw_phases, + phase_switch_supported, + expected, monkeypatch): # setup - ct = ChargeTemplate() + ct = ChargeTemplate(0) plan = ScheduledChargingPlan(phases_to_use=phases_to_use) - calculate_duration_mock = Mock(return_value=calc_duration) + calculate_duration_mock = Mock(side_effect=calc_duration) monkeypatch.setattr(ChargeTemplate, "_calculate_duration", calculate_duration_mock) - evt = Mock(spec=EvTemplate) + evt = Mock(spec=EvTemplate, data=Mock(spec=EvTemplateData, battery_capacity=85)) # execution remaining_time, missing_amount, phases, duration = ct._calc_remaining_time( - plan, 1652688000, 50, evt, 3000, max_hw_phases, phase_switch_supported, ChargingType.AC.value) + plan, 1652688000, 50, evt, 3000, max_hw_phases, phase_switch_supported, ChargingType.AC.value, 2) # end time 16.5.22 10:00 # evaluation @@ -170,36 +178,36 @@ def test_calculate_duration(selected: str, phases: int, expected_duration: float @pytest.mark.parametrize( - "check_duration_return1, check_duration_return2, expected_plan_num", + "end_time_mock, expected_plan_num", [ - pytest.param((-50, False), (60, False), 0, id="too late, but didn't miss date for today"), - pytest.param((-50, True), (60, False), 1, id="too late and missed date for today"), - pytest.param((-50, True), (-60, True), None, id="missed both"), - pytest.param((50, False), (60, False), 0, id="in time, plan 1"), - pytest.param((50, False), (40, False), 1, id="in time, plan 2"), + pytest.param([1652684400, 1652686200, 1652688000], 1, id="1st plan"), # 9:00, 9:30, 10:00 + pytest.param([1652686200, 1652684400, 1652688000], 2, id="2nd plan"), # 9:30, 9:00, 10:00 + pytest.param([1652686200, 1652688000, 1652684400], 3, id="3rd plan"), # 9:30, 10:00, 9:00 + pytest.param([None]*3, 0, id="no plan"), ]) -def test_search_plan(check_duration_return1: Tuple[Optional[float], bool], - check_duration_return2: Tuple[Optional[float], bool], - expected_plan_num: Optional[int], - monkeypatch): +def test_sscheduled_charging_recent_plan(end_time_mock, + expected_plan_num: Optional[int], + monkeypatch): # setup - calculate_duration_mock = Mock(return_value=(100, 200)) - monkeypatch.setattr(ChargeTemplate, "_calculate_duration", calculate_duration_mock) - check_duration_mock = Mock(side_effect=[check_duration_return1, check_duration_return2]) - monkeypatch.setattr(timecheck, "check_end_time", check_duration_mock) + calculate_duration_mock = Mock(return_value=(100, 3000, 3, 50)) + monkeypatch.setattr(ChargeTemplate, "_calc_remaining_time", calculate_duration_mock) + check_end_time_mock = Mock(side_effect=end_time_mock) + monkeypatch.setattr(timecheck, "check_end_time", check_end_time_mock) ct = ChargeTemplate(0) plan_mock_0 = Mock(spec=ScheduledChargingPlan, active=True, current=14, id=0, limit=Limit(selected="amount")) plan_mock_1 = Mock(spec=ScheduledChargingPlan, active=True, current=14, id=1, limit=Limit(selected="amount")) - ct.data.chargemode.scheduled_charging.plans = {"0": plan_mock_0, "1": plan_mock_1} + plan_mock_2 = Mock(spec=ScheduledChargingPlan, active=True, current=14, id=2, limit=Limit(selected="amount")) + ct.data.chargemode.scheduled_charging.plans = {"0": plan_mock_0, "1": plan_mock_1, "2": plan_mock_2} + # execution - plan_data = ct._search_plan(14, 60, EvTemplate(), 3, 200, ChargingType.AC.value) + selected_plan = ct.scheduled_charging_recent_plan( + 60, EvTemplate(), 3, 200, 3, True, ChargingType.AC.value, 1652688000, Mock(spec=ControlParameter)) # evaluation - if expected_plan_num is None: - assert plan_data is None + if selected_plan: + assert selected_plan.plan.id == expected_plan_num else: - assert plan_data.plan.id == expected_plan_num - assert plan_data.duration == 100 + selected_plan = None @pytest.mark.parametrize( diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index e548b4dae1..561ef16545 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -156,7 +156,8 @@ def get_required_current(self, max_phases_hw, phase_switch_supported, charging_type, - chargemde_switch_timestamp) + chargemde_switch_timestamp, + control_parameter) soc_request_interval_offset = 0 if plan_data: # Wenn mit einem neuen Plan geladen wird, muss auch die Energiemenge von neuem gezählt werden. From 444dc3984e6869ef5cc72674d7be2cb4c0bfc416 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 23 Jan 2025 10:06:28 +0100 Subject: [PATCH 13/59] update config --- packages/helpermodules/update_config.py | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index d22e363c20..1106d63a3c 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -1,3 +1,4 @@ +import copy from dataclasses import asdict import datetime import glob @@ -1998,3 +1999,58 @@ def upgrade(topic: str, payload) -> Optional[dict]: 'openWB/io/action/0/config': dataclass_utils.asdict(action)} self._loop_all_received_topics(upgrade) self.__update_topic("openWB/system/datastore_version", 76) + + def upgrade_datastore_76(self) -> None: + def upgrade(topic: str, payload) -> None: + topics = {} + if re.search("openWB/vehicle/template/charge_template/[0-9]+$", topic) is not None: + payload = decode_payload(payload) + charge_template = copy.deepcopy(payload) + # replace bmw soc module by no_module + if payload["chargemode"]["selected"] == "standby": + charge_template["chargemode"]["selected"] = "stop" + if payload["et"]["active"] is True: + charge_template["eco_charging"]["max_price"] = payload["et"]["max_price"] + charge_template["eco_charging"]["limit"] = copy.deepcopy(payload["instant_charging"]["limit"]) + if payload["chargemode"]["selected"] == "instant_charging": + charge_template["chargemode"]["selected"] = "eco_charging" + payload.pop("et") + charge_template["eco_charging"]["phases_to_use"] = self.all_received_topics[ + "openWB/general/chargemode_config/instant_charging/phases_to_use"] + charge_template["instant_charging"]["phases_to_use"] = self.all_received_topics[ + "openWB/general/chargemode_config/instant_charging/phases_to_use"] + charge_template["pv_charging"]["phases_to_use"] = self.all_received_topics[ + "openWB/general/chargemode_config/pv_charging/phases_to_use"] + charge_template["pv_charging"]["phases_to_use_min_soc"] = 3 + if payload["pv_charging"]["max_soc"] == 101: + charge_template["pv_charging"]["limit"]["selected"] = "none" + else: + charge_template["pv_charging"]["limit"]["selected"] = "soc" + charge_template["pv_charging"]["limit"]["soc"] = payload["pv_charging"]["max_soc"] + payload["pv_charging"].pop("max_soc") + topics.update({topic: charge_template}) + + index = get_index(topic) + for scheduled_plan_topic, scheduled_play_payload in self.all_received_topics.keys(): + if re.search(f"openWB/vehicle/template/charge_template/{index}" + "/chargemode/scheduled_charging/plans/[0-9]+$", scheduled_plan_topic) is not None: + payload = decode_payload(scheduled_play_payload) + scheduled_plan = copy.deepcopy(payload) + scheduled_plan["phases_to_use"] = self.all_received_topics[ + "openWB/general/chargemode_config/scheduled_charging/phases_to_use"] + scheduled_plan["phases_to_use_pv"] = self.all_received_topics[ + "openWB/general/chargemode_config/scheduled_charging/phases_to_use_pv"] + scheduled_plan["et_active"] = payload["et"]["active"] + topics.update({scheduled_plan_topic: scheduled_plan}) + for time_plan_topic, time_play_payload in self.all_received_topics.keys(): + if re.search(f"openWB/vehicle/template/charge_template/{index}" + "/time_charging/plans/[0-9]+$", time_plan_topic) is not None: + payload = decode_payload(time_play_payload) + time_plan = copy.deepcopy(payload) + time_plan["phases_to_use"] = self.all_received_topics[ + "openWB/general/chargemode_config/time_charging/phases_to_use"] + topics.update({time_plan_topic: time_plan}) + + Pub().pub(topic, payload) + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 77) From 8ad5ad2b4a73e5fb9845ed96eafeff5947518253 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 30 Jan 2025 08:15:59 +0100 Subject: [PATCH 14/59] update charge template, merge max phases --- packages/helpermodules/update_config.py | 85 ++++++++++++++++--------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 1106d63a3c..9c56dd4bdf 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -17,6 +17,7 @@ from helpermodules import timecheck from helpermodules import hardware_configuration from helpermodules.broker import BrokerClient +from helpermodules.abstract_plans import Limit from helpermodules.constants import NO_ERROR from helpermodules.hardware_configuration import ( get_hardware_configuration_setting, @@ -34,7 +35,7 @@ from control.bat_all import BatConsiderationMode from control.chargepoint.charging_type import ChargingType from control.counter import get_counter_default_config -from control.ev.charge_template import get_charge_template_default +from control.ev.charge_template import EcoCharging, get_charge_template_default from control.ev import ev from control.ev.ev_template import EvTemplateData from control.general import ChargemodeConfig, Prices @@ -2002,55 +2003,77 @@ def upgrade(topic: str, payload) -> Optional[dict]: def upgrade_datastore_76(self) -> None: def upgrade(topic: str, payload) -> None: + def get_new_phases_to_use(topic) -> int: + return min(max_phases_ev, decode_payload(self.all_received_topics[topic])) topics = {} if re.search("openWB/vehicle/template/charge_template/[0-9]+$", topic) is not None: + index = int(get_index(topic)) + # Die Phasenauswahl im Fahrzeugprofil entfällt mit der Phaseneinstellung im Ladeprofil anstelle der + # globalen Phasen. + max_phases_ev = 3 + for ev_topic, ev_payload in self.all_received_topics.items(): + if re.search("openWB/vehicle/[0-9]+/charge_template$", ev_topic) is not None: + assigned_charge_template = decode_payload(ev_payload) + if assigned_charge_template == index: + ev_index = get_index(ev_topic) + ev_template_id = decode_payload( + self.all_received_topics[f"openWB/vehicle/{ev_index}/ev_template"]) + ev_phases = decode_payload( + self.all_received_topics[f"openWB/vehicle/template/ev_template/{ev_template_id}"])["max_phases"] + if ev_phases == 1: + max_phases_ev = ev_phases + payload = decode_payload(payload) charge_template = copy.deepcopy(payload) - # replace bmw soc module by no_module + charge_template["chargemode"]["eco_charging"] = dataclass_utils.asdict(EcoCharging()) if payload["chargemode"]["selected"] == "standby": charge_template["chargemode"]["selected"] = "stop" if payload["et"]["active"] is True: - charge_template["eco_charging"]["max_price"] = payload["et"]["max_price"] - charge_template["eco_charging"]["limit"] = copy.deepcopy(payload["instant_charging"]["limit"]) + charge_template["chargemode"]["eco_charging"]["max_price"] = payload["et"]["max_price"] + charge_template["chargemode"]["eco_charging"]["limit"] = copy.deepcopy( + payload["chargemode"]["instant_charging"]["limit"]) if payload["chargemode"]["selected"] == "instant_charging": charge_template["chargemode"]["selected"] = "eco_charging" - payload.pop("et") - charge_template["eco_charging"]["phases_to_use"] = self.all_received_topics[ - "openWB/general/chargemode_config/instant_charging/phases_to_use"] - charge_template["instant_charging"]["phases_to_use"] = self.all_received_topics[ - "openWB/general/chargemode_config/instant_charging/phases_to_use"] - charge_template["pv_charging"]["phases_to_use"] = self.all_received_topics[ - "openWB/general/chargemode_config/pv_charging/phases_to_use"] - charge_template["pv_charging"]["phases_to_use_min_soc"] = 3 - if payload["pv_charging"]["max_soc"] == 101: - charge_template["pv_charging"]["limit"]["selected"] = "none" + charge_template["chargemode"]["eco_charging"]["phases_to_use"] = get_new_phases_to_use( + "openWB/general/chargemode_config/instant_charging/phases_to_use") + charge_template["chargemode"]["instant_charging"]["phases_to_use"] = get_new_phases_to_use( + "openWB/general/chargemode_config/instant_charging/phases_to_use") + charge_template["chargemode"]["pv_charging"]["phases_to_use"] = get_new_phases_to_use( + "openWB/general/chargemode_config/pv_charging/phases_to_use") + charge_template["chargemode"]["pv_charging"]["phases_to_use_min_soc"] = min(max_phases_ev, 3) + charge_template["chargemode"]["pv_charging"]["limit"] = dataclass_utils.asdict(Limit()) + if payload["chargemode"]["pv_charging"]["max_soc"] == 101: + charge_template["chargemode"]["pv_charging"]["limit"]["selected"] = "none" else: - charge_template["pv_charging"]["limit"]["selected"] = "soc" - charge_template["pv_charging"]["limit"]["soc"] = payload["pv_charging"]["max_soc"] - payload["pv_charging"].pop("max_soc") + charge_template["chargemode"]["pv_charging"]["limit"]["selected"] = "soc" + charge_template["chargemode"]["pv_charging"]["limit"]["soc"] = payload["chargemode"]["pv_charging"]["max_soc"] + charge_template["chargemode"]["pv_charging"].pop("max_soc") topics.update({topic: charge_template}) - index = get_index(topic) - for scheduled_plan_topic, scheduled_play_payload in self.all_received_topics.keys(): + for scheduled_plan_topic, scheduled_plan_payload in self.all_received_topics.items(): if re.search(f"openWB/vehicle/template/charge_template/{index}" "/chargemode/scheduled_charging/plans/[0-9]+$", scheduled_plan_topic) is not None: - payload = decode_payload(scheduled_play_payload) - scheduled_plan = copy.deepcopy(payload) - scheduled_plan["phases_to_use"] = self.all_received_topics[ - "openWB/general/chargemode_config/scheduled_charging/phases_to_use"] - scheduled_plan["phases_to_use_pv"] = self.all_received_topics[ - "openWB/general/chargemode_config/scheduled_charging/phases_to_use_pv"] + scheduled_plan = copy.deepcopy(decode_payload(scheduled_plan_payload)) + scheduled_plan["phases_to_use"] = get_new_phases_to_use( + "openWB/general/chargemode_config/scheduled_charging/phases_to_use") + scheduled_plan["phases_to_use_pv"] = get_new_phases_to_use( + "openWB/general/chargemode_config/scheduled_charging/phases_to_use_pv") scheduled_plan["et_active"] = payload["et"]["active"] topics.update({scheduled_plan_topic: scheduled_plan}) - for time_plan_topic, time_play_payload in self.all_received_topics.keys(): + for time_plan_topic, time_play_payload in self.all_received_topics.items(): if re.search(f"openWB/vehicle/template/charge_template/{index}" "/time_charging/plans/[0-9]+$", time_plan_topic) is not None: - payload = decode_payload(time_play_payload) - time_plan = copy.deepcopy(payload) - time_plan["phases_to_use"] = self.all_received_topics[ - "openWB/general/chargemode_config/time_charging/phases_to_use"] + time_plan = copy.deepcopy(decode_payload(time_play_payload)) + time_plan["phases_to_use"] = get_new_phases_to_use( + "openWB/general/chargemode_config/time_charging/phases_to_use") topics.update({time_plan_topic: time_plan}) - Pub().pub(topic, payload) + charge_template.pop("et") + for evt_topic, evt_payload in self.all_received_topics.items(): + if re.search("openWB/vehicle/template/ev_template/[0-9]+$", evt_topic) is not None: + ev_template = decode_payload(evt_payload) + ev_template.pop("max_phases") + topics.update({evt_topic: ev_template}) + return topics self._loop_all_received_topics(upgrade) self.__update_topic("openWB/system/datastore_version", 77) From 1b275840f61256d54cd62d4afbda51887ed85035 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 30 Jan 2025 11:29:51 +0100 Subject: [PATCH 15/59] fixes --- packages/control/chargepoint/chargepoint.py | 4 ++-- packages/control/ev/charge_template.py | 17 +++++++++-------- packages/control/ev/ev.py | 3 ++- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index ea78656e95..f848068d67 100644 --- a/packages/control/chargepoint/chargepoint.py +++ b/packages/control/chargepoint/chargepoint.py @@ -488,8 +488,7 @@ def initiate_phase_switch(self): def get_phases_by_selected_chargemode(self, phases_chargemode: int) -> int: charging_ev = self.data.set.charging_ev_data - if (phases_chargemode is None or - (self.data.config.auto_phase_switch_hw is False and self.data.get.charge_state) or + if ((self.data.config.auto_phase_switch_hw is False and self.data.get.charge_state) or self.data.control_parameter.failed_phase_switches > self.MAX_FAILED_PHASE_SWITCHES): # Wenn keine Umschaltung verbaut ist, die Phasenzahl nehmen, mit der geladen wird. Damit werden zB auch # einphasige EV an dreiphasigen openWBs korrekt berücksichtigt. @@ -634,6 +633,7 @@ def update(self, ev_list: Dict[str, Ev]) -> None: self.cp_ev_support_phase_switch(), self.template.data.charging_type, self.data.set.log.timestamp_start_charging) + phases = self.get_phases_by_selected_chargemode(phases) phases = self.set_phases(phases) self._pub_connected_vehicle(charging_ev) required_current = self.chargepoint_module.add_conversion_loss_to_current(required_current) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 6626b25695..d481f96cc1 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -183,7 +183,8 @@ def time_charging(self, return current, sub_mode, message, id, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) - return 0, "stop", "Keine Ladung, da da ein interner Fehler aufgetreten ist: "+traceback.format_exc(), None + return (0, "stop", "Keine Ladung, da da ein interner Fehler aufgetreten ist: "+traceback.format_exc(), None, + 0) SOC_REACHED = "Keine Ladung, da der Soc bereits erreicht wurde." AMOUNT_REACHED = "Keine Ladung, da die Energiemenge bereits geladen wurde." @@ -216,7 +217,7 @@ def instant_charging(self, return current, sub_mode, message, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) - return 0, "stop", "Keine Ladung, da da ein interner Fehler aufgetreten ist: "+traceback.format_exc() + return 0, "stop", "Keine Ladung, da da ein interner Fehler aufgetreten ist: "+traceback.format_exc(), 0 PV_CHARGING_SOC_CHARGING = ("Ladung evtl. auch ohne PV-Überschuss, da der Mindest-SoC des Fahrzeugs noch nicht " "erreicht wurde.") @@ -225,7 +226,8 @@ def instant_charging(self, def pv_charging(self, soc: Optional[float], min_current: int, - charging_type: str) -> Tuple[int, str, Optional[str], int]: + charging_type: str, + used_amount: float) -> Tuple[int, str, Optional[str], int]: """ prüft, ob Min-oder Max-Soc erreicht wurden und setzt entsprechend den Ladestrom. """ message = None @@ -239,9 +241,8 @@ def pv_charging(self, current = 0 sub_mode = "stop" message = self.SOC_REACHED - elif (pv_charging.limit.selected == "amount" and - pv_charging >= self.data.chargemode.instant_charging.limit.amount): - current = 0, + elif pv_charging.limit.selected == "amount" and used_amount >= pv_charging.limit.amount: + current = 0 sub_mode = "stop" message = self.AMOUNT_REACHED else: @@ -266,7 +267,7 @@ def pv_charging(self, return current, sub_mode, message, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) - return 0, "stop", "Keine Ladung, da ein interner Fehler aufgetreten ist: "+traceback.format_exc() + return 0, "stop", "Keine Ladung, da ein interner Fehler aufgetreten ist: "+traceback.format_exc(), 1 def eco_charging(self, soc: Optional[float], @@ -302,7 +303,7 @@ def eco_charging(self, return current, sub_mode, message, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) - return 0, "stop", "Keine Ladung, da ein interner Fehler aufgetreten ist: "+traceback.format_exc() + return 0, "stop", "Keine Ladung, da ein interner Fehler aufgetreten ist: "+traceback.format_exc(), 0 def scheduled_charging_recent_plan(self, soc: float, diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 561ef16545..064fdb36db 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -216,8 +216,9 @@ def get_required_current(self, used_amount, charging_type) elif self.charge_template.data.chargemode.selected == "pv_charging": + used_amount = 0 required_current, submode, message, phases = self.charge_template.pv_charging( - self.data.get.soc, control_parameter.min_current, charging_type) + self.data.get.soc, control_parameter.min_current, charging_type, used_amount) elif self.charge_template.data.chargemode.selected == "eco_charging": required_current, submode, message, phases = self.charge_template.eco_charging( self.data.get.soc, control_parameter.min_current, charging_type) From f5546674d42a9e09f6200138160ab50443a7fbb3 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 30 Jan 2025 16:04:25 +0100 Subject: [PATCH 16/59] fixes --- packages/control/chargepoint/chargepoint.py | 6 +-- .../control/chargepoint/control_parameter.py | 4 -- packages/control/ev/charge_template.py | 46 +++++++++++-------- packages/control/ev/ev.py | 40 ++++------------ packages/helpermodules/setdata.py | 4 +- packages/helpermodules/timecheck.py | 13 ++++-- packages/helpermodules/timecheck_test.py | 2 +- 7 files changed, 48 insertions(+), 67 deletions(-) diff --git a/packages/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index f848068d67..adfb9eafdf 100644 --- a/packages/control/chargepoint/chargepoint.py +++ b/packages/control/chargepoint/chargepoint.py @@ -304,8 +304,6 @@ def reset_control_parameter_at_charge_stop(self) -> None: # Wenn die Ladung zB wegen Autolock gestoppt wird, Zählerstände beibehalten, damit nicht nochmal die Ladung # gestartet wird. control_parameter = control_parameter_factory() - control_parameter.imported_at_plan_start = self.data.control_parameter.imported_at_plan_start - control_parameter.imported_instant_charging = self.data.control_parameter.imported_instant_charging self.data.control_parameter = control_parameter def initiate_control_pilot_interruption(self): @@ -628,11 +626,11 @@ def update(self, ev_list: Dict[str, Ev]) -> None: max_phase_hw = self.get_max_phase_hw() state, message_ev, submode, required_current, phases = charging_ev.get_required_current( self.data.control_parameter, - self.data.get.imported, max_phase_hw, self.cp_ev_support_phase_switch(), self.template.data.charging_type, - self.data.set.log.timestamp_start_charging) + self.data.set.log.timestamp_start_charging, + self.data.set.log.imported_since_plugged) phases = self.get_phases_by_selected_chargemode(phases) phases = self.set_phases(phases) self._pub_connected_vehicle(charging_ev) diff --git a/packages/control/chargepoint/control_parameter.py b/packages/control/chargepoint/control_parameter.py index f419f4f012..79abfd0137 100644 --- a/packages/control/chargepoint/control_parameter.py +++ b/packages/control/chargepoint/control_parameter.py @@ -13,10 +13,6 @@ class ControlParameter: "topic": "control_parameter/chargemode"}) current_plan: Optional[str] = field(default=None, metadata={"topic": "control_parameter/current_plan"}) failed_phase_switches: int = field(default=0, metadata={"topic": "control_parameter/failed_phase_switches"}) - imported_at_plan_start: Optional[float] = field( - default=None, metadata={"topic": "control_parameter/imported_at_plan_start"}) - imported_instant_charging: Optional[float] = field( - default=None, metadata={"topic": "control_parameter/imported_instant_charging"}) limit: Optional[LimitingValue] = field(default=None, metadata={"topic": "control_parameter/limit"}) min_current: int = field(default=6, metadata={"topic": "control_parameter/min_current"}) phases: int = field(default=0, metadata={"topic": "control_parameter/phases"}) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index d481f96cc1..2e36d3e761 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -137,7 +137,7 @@ class ChargeTemplate: BUFFER = -1200 # nach mehr als 20 Min Überschreitung wird der Termin als verpasst angesehen CHARGING_PRICE_EXCEEDED = ("Keine Ladung, da der aktuelle Strompreis über dem maximalen Strompreis liegt. " + "Falls vorhanden wird mit EVU-Überschuss geladen.") - CHARGING_PRICE_LOW = "Ladung, da der aktuelle Strompreis unter dem maximalen Strompreis liegt." + CHARGING_PRICE_LOW = "Laden, da der aktuelle Strompreis unter dem maximalen Strompreis liegt." TIME_CHARGING_NO_PLAN_CONFIGURED = "Keine Ladung, da keine Zeitfenster für Zeitladen konfiguriert sind." TIME_CHARGING_NO_PLAN_ACTIVE = "Keine Ladung, da kein Zeitfenster für Zeitladen aktiv ist." @@ -191,7 +191,7 @@ def time_charging(self, def instant_charging(self, soc: Optional[float], - imported_instant_charging: float, + used_amount: float, charging_type: str) -> Tuple[int, str, Optional[str], int]: """ prüft, ob die Lademengenbegrenzung erreicht wurde und setzt entsprechend den Ladestrom. """ @@ -210,7 +210,7 @@ def instant_charging(self, sub_mode = "stop" message = self.SOC_REACHED elif instant_charging.limit.selected == "amount": - if imported_instant_charging >= self.data.chargemode.instant_charging.limit.amount: + if used_amount >= self.data.chargemode.instant_charging.limit.amount: current = 0 sub_mode = "stop" message = self.AMOUNT_REACHED @@ -272,7 +272,8 @@ def pv_charging(self, def eco_charging(self, soc: Optional[float], min_current: int, - charging_type: str) -> Tuple[int, str, Optional[str], int]: + charging_type: str, + used_amount: float) -> Tuple[int, str, Optional[str], int]: """ prüft, ob Min-oder Max-Soc erreicht wurden und setzt entsprechend den Ladestrom. """ message = None @@ -287,17 +288,17 @@ def eco_charging(self, sub_mode = "stop" message = self.SOC_REACHED elif (eco_charging.limit.selected == "amount" and - eco_charging >= self.data.chargemode.instant_charging.limit.amount): - current = 0, + used_amount >= self.data.chargemode.instant_charging.limit.amount): + current = 0 sub_mode = "stop" message = self.AMOUNT_REACHED elif data.data.optional_data.et_provider_available(): if data.data.optional_data.et_price_lower_than_limit(eco_charging.max_price): - current = min_current - message = self.CHARGING_PRICE_EXCEEDED - else: sub_mode = "instant_charging" message = self.CHARGING_PRICE_LOW + else: + current = min_current + message = self.CHARGING_PRICE_EXCEEDED else: current = min_current return current, sub_mode, message, phases @@ -313,7 +314,7 @@ def scheduled_charging_recent_plan(self, max_hw_phases: int, phase_switch_supported: bool, charging_type: str, - chargemde_switch_timestamp: float, + chargemode_switch_timestamp: float, control_parameter: ControlParameter) -> Optional[SelectedPlan]: plans_diff_end_date = [] for p in self.data.chargemode.scheduled_charging.plans.values(): @@ -322,9 +323,9 @@ def scheduled_charging_recent_plan(self, raise ValueError("Um Zielladen mit SoC-Ziel nutzen zu können, bitte ein SoC-Modul konfigurieren " f"oder im Plan {p.name} als Begrenzung Energie einstellen.") try: - plans_diff_end_date.update( - {p.id: timecheck.check_end_time(p.time, chargemde_switch_timestamp)}) - + plans_diff_end_date.append( + {p.id: timecheck.check_end_time(p, chargemode_switch_timestamp)}) + log.debug("Verbleibende Zeit bis zum Zieltermin [s]: "+str(plans_diff_end_date)) except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) if plans_diff_end_date: @@ -335,7 +336,7 @@ def scheduled_charging_recent_plan(self, plan_id = list(plan_dict.keys())[0] plan_end_time = list(plan_dict.values())[0] - plan = self.data.chargemode.scheduled_charging.plans[plan_id] + plan = self.data.chargemode.scheduled_charging.plans[str(plan_id)] remaining_time, missing_amount, phases, duration = self._calc_remaining_time( plan, plan_end_time, soc, ev_template, used_amount, max_hw_phases, phase_switch_supported, @@ -363,32 +364,39 @@ def _calc_remaining_time(self, if max_hw_phases == 1: duration, missing_amount = self._calculate_duration( plan, soc, ev_template.data.battery_capacity, used_amount, 1, charging_type, ev_template) + remaining_time = plan_end_time - duration phases = 1 elif phase_switch_supported is False: duration, missing_amount = self._calculate_duration( plan, soc, ev_template.data.battery_capacity, used_amount, control_parameter_phases, charging_type, ev_template) phases = control_parameter_phases + remaining_time = plan_end_time - duration else: duration_3p, missing_amount = self._calculate_duration( plan, soc, ev_template.data.battery_capacity, used_amount, 3, charging_type, ev_template) + remaining_time_3p = plan_end_time - duration_3p duration_1p, missing_amount = self._calculate_duration( plan, soc, ev_template.data.battery_capacity, used_amount, 1, charging_type, ev_template) - if duration_1p < 0: + remaining_time_1p = plan_end_time - duration_1p + if remaining_time_1p < 0: # Zeit reicht nicht mehr für einphasiges Laden + remaining_time = remaining_time_3p duration = duration_3p phases = 3 else: + remaining_time = remaining_time_1p duration = duration_1p phases = 1 + log.debug(f"Dauer 1p: {duration_1p}, Dauer 3p: {duration_3p}") elif plan.phases_to_use == 3 or plan.phases_to_use == 1: duration, missing_amount = self._calculate_duration( plan, soc, ev_template.data.battery_capacity, used_amount, plan.phases_to_use, charging_type, ev_template) + remaining_time = plan_end_time - duration phases = plan.phases_to_use - start_time = plan_end_time - duration - remaining_time = start_time - timecheck.create_timestamp() + log.debug(f"Verbleibende Zeit bis zum Ladestart [s]:{remaining_time}, Dauer [h]: {duration/3600}") return remaining_time, missing_amount, phases, duration def _calculate_duration(self, @@ -425,8 +433,7 @@ def _calculate_duration(self, SCHEDULED_CHARGING_NO_DATE_PENDING = "Kein Zielladen, da kein Ziel-Termin ansteht." SCHEDULED_CHARGING_USE_PV = ("Laden startet {}. Falls vorhanden, " "wird mit Überschuss geladen.") - SCHEDULED_CHARGING_MAX_CURRENT = ("Zielladen mit {}A. Der Ladestrom wurde erhöht, um den Zieltermin zu erreichen. " - "Es wird bis max. 20 Minuten nach dem angegebenen Zieltermin geladen.") + SCHEDULED_CHARGING_MAX_CURRENT = "Zielladen mit {}A. Der Ladestrom wurde erhöht, um das Ziel zu erreichen." SCHEDULED_CHARGING_LIMITED_BY_SOC = 'einen SoC von {}%' SCHEDULED_CHARGING_LIMITED_BY_AMOUNT = '{}kWh geladene Energie' SCHEDULED_CHARGING_IN_TIME = ('Zielladen mit mindestens {}A, um {} um {} zu erreichen. Falls vorhanden wird ' @@ -482,7 +489,6 @@ def scheduled_charging_calc_current(self, current = plan_current submode = "instant_charging" # weniger als die berechnete Zeit verfügbar - # Ladestart wurde um maximal 20 Min verpasst. elif selected_plan.remaining_time <= 0 - soc_request_interval_offset: if selected_plan.duration + selected_plan.remaining_time < 0: current = max_current diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 064fdb36db..5ec6d4a706 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -116,11 +116,11 @@ def soc_interval_expired(self, vehicle_update_data: VehicleUpdateData) -> bool: def get_required_current(self, control_parameter: ControlParameter, - imported: float, max_phases_hw: int, phase_switch_supported: bool, charging_type: str, - chargemde_switch_timestamp: float) -> Tuple[bool, Optional[str], str, float, int]: + chargemode_switch_timestamp: float, + imported_since_plugged: float) -> Tuple[bool, Optional[str], str, float, int]: """ ermittelt, ob und mit welchem Strom das EV geladen werden soll (unabhängig vom Lastmanagement) Parameter @@ -145,31 +145,23 @@ def get_required_current(self, state = True try: if self.charge_template.data.chargemode.selected == "scheduled_charging": - if control_parameter.imported_at_plan_start is None: - control_parameter.imported_at_plan_start = imported - used_amount = imported - control_parameter.imported_at_plan_start plan_data = self.charge_template.scheduled_charging_recent_plan( self.data.get.soc, self.ev_template, control_parameter.phases, - used_amount, + imported_since_plugged, max_phases_hw, phase_switch_supported, charging_type, - chargemde_switch_timestamp, + chargemode_switch_timestamp, control_parameter) soc_request_interval_offset = 0 if plan_data: - # Wenn mit einem neuen Plan geladen wird, muss auch die Energiemenge von neuem gezählt werden. - if (self.charge_template.data.chargemode.scheduled_charging.plans[str(plan_data.id)].limit. - selected == "amount" and - plan_data.plan.id != control_parameter.current_plan): - control_parameter.imported_at_plan_start = imported # Wenn der SoC ein paar Minuten alt ist, kann der Termin trotzdem gehalten werden. # Zielladen kann nicht genauer arbeiten, als das Abfrageintervall vom SoC. if (self.soc_module and self.charge_template.data.chargemode. - scheduled_charging.plans[str(plan_data.id)].limit.selected == "soc"): + scheduled_charging.plans[str(plan_data.plan.id)].limit.selected == "soc"): soc_request_interval_offset = self.soc_module.general_config.request_interval_charging control_parameter.current_plan = plan_data.plan.id else: @@ -177,7 +169,7 @@ def get_required_current(self, required_current, submode, message, phases = self.charge_template.scheduled_charging_calc_current( plan_data, self.data.get.soc, - used_amount, + imported_since_plugged, control_parameter.phases, control_parameter.min_current, soc_request_interval_offset, @@ -187,41 +179,29 @@ def get_required_current(self, # Wenn Zielladen auf Überschuss wartet, prüfen, ob Zeitladen aktiv ist. if (submode != "instant_charging" and self.charge_template.data.time_charging.active): - if control_parameter.imported_at_plan_start is None: - control_parameter.imported_at_plan_start = imported - used_amount = imported - control_parameter.imported_at_plan_start tmp_current, tmp_submode, tmp_message, plan_id, phases = self.charge_template.time_charging( self.data.get.soc, - used_amount, + imported_since_plugged, charging_type ) # Info vom Zielladen erhalten message = f"{message or ''} {tmp_message or ''}".strip() if tmp_current > 0: - # Wenn mit einem neuen Plan geladen wird, muss auch die Energiemenge von neuem gezählt werden. - if plan_id != control_parameter.current_plan: - control_parameter.imported_at_plan_start = imported control_parameter.current_plan = plan_id required_current = tmp_current submode = tmp_submode if (required_current == 0) or (required_current is None): if self.charge_template.data.chargemode.selected == "instant_charging": - # Wenn der Submode auf stop gestellt wird, wird auch die Energiemenge seit Wechsel des Modus - # zurückgesetzt, dann darf nicht die Energiemenge erneute geladen werden. - if control_parameter.imported_instant_charging is None: - control_parameter.imported_instant_charging = imported - used_amount = imported - control_parameter.imported_instant_charging required_current, submode, message, phases = self.charge_template.instant_charging( self.data.get.soc, - used_amount, + imported_since_plugged, charging_type) elif self.charge_template.data.chargemode.selected == "pv_charging": - used_amount = 0 required_current, submode, message, phases = self.charge_template.pv_charging( - self.data.get.soc, control_parameter.min_current, charging_type, used_amount) + self.data.get.soc, control_parameter.min_current, charging_type, imported_since_plugged) elif self.charge_template.data.chargemode.selected == "eco_charging": required_current, submode, message, phases = self.charge_template.eco_charging( - self.data.get.soc, control_parameter.min_current, charging_type) + self.data.get.soc, control_parameter.min_current, charging_type, imported_since_plugged) elif self.charge_template.data.chargemode.selected == "stop": required_current, submode, message = self.charge_template.stop() phases = control_parameter.phases or max_phases_hw diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 21e8a86a4c..1a4799ef21 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -579,9 +579,7 @@ def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, bool) elif "/control_parameter/current_plan" in msg.topic: self._validate_value(msg, int) - elif ("/control_parameter/imported_instant_charging" in msg.topic or - "/control_parameter/imported_at_plan_start" in msg.topic or - "/control_parameter/min_current" in msg.topic or + elif ("/control_parameter/min_current" in msg.topic or "/control_parameter/timestamp_switch_on_off" in msg.topic or "/control_parameter/timestamp_charge_start" in msg.topic or "/control_parameter/timestamp_last_phase_switch" in msg.topic): diff --git a/packages/helpermodules/timecheck.py b/packages/helpermodules/timecheck.py index 30b6079041..d3767fdfb5 100644 --- a/packages/helpermodules/timecheck.py +++ b/packages/helpermodules/timecheck.py @@ -112,7 +112,7 @@ def is_timeframe_valid(now: datetime.datetime, begin: datetime.datetime, end: da return state -def check_end_time(plan: ScheduledChargingPlan, chargemde_switch_timestamp: float) -> Optional[float]: +def check_end_time(plan: ScheduledChargingPlan, chargemode_switch_timestamp: Optional[float]) -> Optional[float]: """ prüft, ob der in angegebene Zeitpunkt abzüglich der Dauer jetzt ist. Um etwas Puffer zu haben, werden bei Überschreiten des Zeitpunkts die nachfolgenden 20 Min auch noch als Ladezeit zurückgegeben. @@ -122,7 +122,10 @@ def check_end_time(plan: ScheduledChargingPlan, chargemde_switch_timestamp: floa neg: Zeitpunkt vorbei pos: verbleibende Sekunden """ - + def missed_date_still_active(remaining_time: float) -> bool: + return (chargemode_switch_timestamp and + remaining_time.total_seconds() < 0 and + end.timestamp() < chargemode_switch_timestamp) now = datetime.datetime.today() end = datetime.datetime.strptime(plan.time, '%H:%M') remaining_time = None @@ -133,7 +136,7 @@ def check_end_time(plan: ScheduledChargingPlan, chargemde_switch_timestamp: floa elif plan.frequency.selected == "daily": end = end.replace(now.year, now.month, now.day) remaining_time = end - now - if remaining_time.total_seconds() < 0 and end.timestamp() < chargemde_switch_timestamp: + if missed_date_still_active(remaining_time): # Wenn auf Zielladen umgeschaltet wurde und der Termin noch nicht vorbei war, noch auf diesen Termin laden. end = end + datetime.timedelta(days=1) remaining_time = end - now @@ -142,13 +145,13 @@ def check_end_time(plan: ScheduledChargingPlan, chargemde_switch_timestamp: floa raise ValueError("Es muss mindestens ein Tag ausgewählt werden.") end = end.replace(now.year, now.month, now.day + _get_next_charging_day(plan.frequency.weekly, now.weekday())) remaining_time = end - now - if remaining_time.total_seconds() < 0 and end.timestamp() < chargemde_switch_timestamp: + if missed_date_still_active(remaining_time): end = end.replace(now.year, now.month, now.day + _get_next_charging_day(plan.frequency.weekly, now.weekday()+1)+1) remaining_time = end - now else: raise TypeError(f'Unbekannte Häufigkeit {plan.frequency.selected}') - if end.timestamp() < chargemde_switch_timestamp: + if chargemode_switch_timestamp and end.timestamp() < chargemode_switch_timestamp: # Als auf Zielladen umgeschaltet wurde, war der Termin schon vorbei return None else: diff --git a/packages/helpermodules/timecheck_test.py b/packages/helpermodules/timecheck_test.py index 9d4f740e5e..d5aaaf4a8f 100644 --- a/packages/helpermodules/timecheck_test.py +++ b/packages/helpermodules/timecheck_test.py @@ -46,7 +46,7 @@ def test_check_end_time(time: str, # execution remaining_time = timecheck.check_end_time( - plan, chargemde_switch_timestamp=1652680800) # angesteckt am 16.5.22 um 8:00 + plan, chargemode_switch_timestamp=1652680800) # angesteckt am 16.5.22 um 8:00 # evaluation assert remaining_time == expected_remaining_time From 10f4d13344fb4b5da0942548083cf1183dd17e12 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 31 Jan 2025 07:25:35 +0100 Subject: [PATCH 17/59] fix --- packages/control/ev/charge_template.py | 2 -- packages/control/ev/ev.py | 9 +++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 2e36d3e761..ce9051a7ba 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -179,7 +179,6 @@ def time_charging(self, message = self.TIME_CHARGING_NO_PLAN_CONFIGURED current = 0 sub_mode = "stop" - log.debug(message) return current, sub_mode, message, id, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) @@ -263,7 +262,6 @@ def pv_charging(self, current = min_pv_current sub_mode = "instant_charging" message = self.PV_CHARGING_MIN_CURRENT_CHARGING - return current, sub_mode, message, phases except Exception: log.exception("Fehler im ev-Modul "+str(self.ct_num)) diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 5ec6d4a706..206ca0a500 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -192,19 +192,20 @@ def get_required_current(self, submode = tmp_submode if (required_current == 0) or (required_current is None): if self.charge_template.data.chargemode.selected == "instant_charging": - required_current, submode, message, phases = self.charge_template.instant_charging( + required_current, submode, tmp_message, phases = self.charge_template.instant_charging( self.data.get.soc, imported_since_plugged, charging_type) elif self.charge_template.data.chargemode.selected == "pv_charging": - required_current, submode, message, phases = self.charge_template.pv_charging( + required_current, submode, tmp_message, phases = self.charge_template.pv_charging( self.data.get.soc, control_parameter.min_current, charging_type, imported_since_plugged) elif self.charge_template.data.chargemode.selected == "eco_charging": - required_current, submode, message, phases = self.charge_template.eco_charging( + required_current, submode, tmp_message, phases = self.charge_template.eco_charging( self.data.get.soc, control_parameter.min_current, charging_type, imported_since_plugged) elif self.charge_template.data.chargemode.selected == "stop": - required_current, submode, message = self.charge_template.stop() + required_current, submode, tmp_message = self.charge_template.stop() phases = control_parameter.phases or max_phases_hw + message = f"{message or ''} {tmp_message or ''}".strip() if submode == "stop" or (self.charge_template.data.chargemode.selected == "stop"): state = False if phases is None: From 6da6c7459965f75a2a8bfd0f4af92e4ffa3b5956 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 31 Jan 2025 08:20:17 +0100 Subject: [PATCH 18/59] fix tests --- packages/control/ev/charge_template.py | 35 +++++++++--------- packages/control/ev/charge_template_test.py | 41 +++++++++++---------- packages/helpermodules/update_config.py | 6 ++- 3 files changed, 43 insertions(+), 39 deletions(-) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index ce9051a7ba..d1588a0341 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -328,23 +328,24 @@ def scheduled_charging_recent_plan(self, log.exception("Fehler im ev-Modul "+str(self.ct_num)) if plans_diff_end_date: # ermittle den Key vom kleinsten value in plans_diff_end_date - plan_dict = min(plans_diff_end_date, key=lambda x: x.get( - 'plans_diff_end_date', float('inf'))) - if plan_dict: - plan_id = list(plan_dict.keys())[0] - plan_end_time = list(plan_dict.values())[0] - - plan = self.data.chargemode.scheduled_charging.plans[str(plan_id)] - - remaining_time, missing_amount, phases, duration = self._calc_remaining_time( - plan, plan_end_time, soc, ev_template, used_amount, max_hw_phases, phase_switch_supported, - charging_type, control_parameter.phases) - - return SelectedPlan(remaining_time=remaining_time, - duration=duration, - missing_amount=missing_amount, - phases=phases, - plan=plan) + filtered_plans = [d for d in plans_diff_end_date if list(d.values())[0] is not None] + if filtered_plans: + plan_dict = min(filtered_plans, key=lambda x: list(x.values())[0]) + if plan_dict: + plan_id = list(plan_dict.keys())[0] + plan_end_time = list(plan_dict.values())[0] + + plan = self.data.chargemode.scheduled_charging.plans[str(plan_id)] + + remaining_time, missing_amount, phases, duration = self._calc_remaining_time( + plan, plan_end_time, soc, ev_template, used_amount, max_hw_phases, phase_switch_supported, + charging_type, control_parameter.phases) + + return SelectedPlan(remaining_time=remaining_time, + duration=duration, + missing_amount=missing_amount, + phases=phases, + plan=plan) else: return None diff --git a/packages/control/ev/charge_template_test.py b/packages/control/ev/charge_template_test.py index 548d1500ba..beb2bd3c15 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -98,17 +98,18 @@ def test_instant_charging(selected: str, current_soc: float, used_amount: float, @pytest.mark.parametrize( - "min_soc, min_current, current_soc, expected", + "min_soc, min_current, limit_selected, current_soc, used_amount, expected", [ - pytest.param(0, 0, 100, (0, "stop", ChargeTemplate.SOC_REACHED, 0), id="max soc reached"), - pytest.param(15, 0, 14, (10, "instant_charging", ChargeTemplate.PV_CHARGING_SOC_CHARGING, 3), + pytest.param(0, 0, "amount", 14, 1500, (0, "stop", ChargeTemplate.AMOUNT_REACHED, 0), id="max amount reached"), + pytest.param(0, 0, "soc", 100, 900, (0, "stop", ChargeTemplate.SOC_REACHED, 0), id="max soc reached"), + pytest.param(15, 0, None, 14, 900, (10, "instant_charging", ChargeTemplate.PV_CHARGING_SOC_CHARGING, 3), id="min soc not reached"), - pytest.param(15, 0, None, (6, "pv_charging", None, 0), id="soc not defined"), - pytest.param(15, 8, 15, (8, "instant_charging", ChargeTemplate.PV_CHARGING_MIN_CURRENT_CHARGING, 0), + pytest.param(15, 0, None, None, 900, (6, "pv_charging", None, 0), id="soc not defined"), + pytest.param(15, 8, None, 15, 900, (8, "instant_charging", ChargeTemplate.PV_CHARGING_MIN_CURRENT_CHARGING, 0), id="min current configured"), - pytest.param(15, 0, 15, (6, "pv_charging", None, 0), id="bare pv charging"), + pytest.param(15, 0, None, 15, 900, (6, "pv_charging", None, 0), id="bare pv charging"), ]) -def test_pv_charging(min_soc: int, min_current: int, current_soc: float, +def test_pv_charging(min_soc: int, min_current: int, limit_selected: str, current_soc: float, used_amount: float, expected: Tuple[int, str, Optional[str], int]): # setup ct = ChargeTemplate(0) @@ -116,12 +117,12 @@ def test_pv_charging(min_soc: int, min_current: int, current_soc: float, ct.data.chargemode.pv_charging.min_current = min_current ct.data.chargemode.pv_charging.phases_to_use = 0 ct.data.chargemode.pv_charging.phases_to_use_min_soc = 3 - ct.data.chargemode.pv_charging.limit.selected = "soc" + ct.data.chargemode.pv_charging.limit.selected = limit_selected ct.data.chargemode.pv_charging.limit.soc = 90 data.data.bat_all_data.data.config.configured = True # execution - ret = ct.pv_charging(current_soc, 6, ChargingType.AC.value) + ret = ct.pv_charging(current_soc, 6, ChargingType.AC.value, used_amount) # evaluation assert ret == expected @@ -129,13 +130,13 @@ def test_pv_charging(min_soc: int, min_current: int, current_soc: float, @pytest.mark.parametrize("phases_to_use, calc_duration, max_hw_phases, phase_switch_supported, expected", [ - pytest.param(0, [(1000, 3)], 1, True, (3748, 3, 1, 1000), id="automatic, one hw phase"), - pytest.param(0, [(1000, 3)], 3, False, (3748, 3, 2, 1000), + pytest.param(0, [(1000, 3)], 1, True, (5000, 3, 1, 1000), id="automatic, one hw phase"), + pytest.param(0, [(1000, 3)], 3, False, (5000, 3, 2, 1000), id="automatic, no phase switch"), - pytest.param(0, [(1000, 3), (-50, 3)], 3, True, (3748, 3, 3, 1000), id="automatic, 3p"), - pytest.param(0, [(5000, 3), (1500, 3)], 3, True, (3248, 3, 1, 1500), id="automatic, 1p"), - pytest.param(3, [(5000, 3)], 3, True, (-252, 3, 3, 5000), id="3p"), - pytest.param(1, [(5000, 3)], 3, True, (-252, 3, 1, 5000), id="1p"), + pytest.param(0, [(1000, 3), (7000, 3)], 3, True, (5000, 3, 3, 1000), id="automatic, 3p"), + pytest.param(0, [(500, 3), (1500, 3)], 3, True, (4500, 3, 1, 1500), id="automatic, 1p"), + pytest.param(3, [(5000, 3)], 3, True, (1000, 3, 3, 5000), id="3p"), + pytest.param(1, [(5000, 3)], 3, True, (1000, 3, 1, 5000), id="1p"), ]) def test_calc_remaining_time(phases_to_use, calc_duration, @@ -151,7 +152,7 @@ def test_calc_remaining_time(phases_to_use, # execution remaining_time, missing_amount, phases, duration = ct._calc_remaining_time( - plan, 1652688000, 50, evt, 3000, max_hw_phases, phase_switch_supported, ChargingType.AC.value, 2) + plan, 6000, 50, evt, 3000, max_hw_phases, phase_switch_supported, ChargingType.AC.value, 2) # end time 16.5.22 10:00 # evaluation @@ -180,16 +181,16 @@ def test_calculate_duration(selected: str, phases: int, expected_duration: float @pytest.mark.parametrize( "end_time_mock, expected_plan_num", [ - pytest.param([1652684400, 1652686200, 1652688000], 1, id="1st plan"), # 9:00, 9:30, 10:00 - pytest.param([1652686200, 1652684400, 1652688000], 2, id="2nd plan"), # 9:30, 9:00, 10:00 - pytest.param([1652686200, 1652688000, 1652684400], 3, id="3rd plan"), # 9:30, 10:00, 9:00 + pytest.param([1000, 1500, 2000], 0, id="1st plan"), + pytest.param([1500, 1000, 2000], 1, id="2nd plan"), + pytest.param([1500, 2000, 1000], 2, id="3rd plan"), pytest.param([None]*3, 0, id="no plan"), ]) def test_sscheduled_charging_recent_plan(end_time_mock, expected_plan_num: Optional[int], monkeypatch): # setup - calculate_duration_mock = Mock(return_value=(100, 3000, 3, 50)) + calculate_duration_mock = Mock(return_value=(100, 3000, 3, 500)) monkeypatch.setattr(ChargeTemplate, "_calc_remaining_time", calculate_duration_mock) check_end_time_mock = Mock(side_effect=end_time_mock) monkeypatch.setattr(timecheck, "check_end_time", check_end_time_mock) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 9c56dd4bdf..856a48ff14 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -2019,7 +2019,8 @@ def get_new_phases_to_use(topic) -> int: ev_template_id = decode_payload( self.all_received_topics[f"openWB/vehicle/{ev_index}/ev_template"]) ev_phases = decode_payload( - self.all_received_topics[f"openWB/vehicle/template/ev_template/{ev_template_id}"])["max_phases"] + self.all_received_topics[ + f"openWB/vehicle/template/ev_template/{ev_template_id}"])["max_phases"] if ev_phases == 1: max_phases_ev = ev_phases @@ -2046,7 +2047,8 @@ def get_new_phases_to_use(topic) -> int: charge_template["chargemode"]["pv_charging"]["limit"]["selected"] = "none" else: charge_template["chargemode"]["pv_charging"]["limit"]["selected"] = "soc" - charge_template["chargemode"]["pv_charging"]["limit"]["soc"] = payload["chargemode"]["pv_charging"]["max_soc"] + charge_template["chargemode"]["pv_charging"]["limit"]["soc"] = payload[ + "chargemode"]["pv_charging"]["max_soc"] charge_template["chargemode"]["pv_charging"].pop("max_soc") topics.update({topic: charge_template}) From 1f0da9ae8a545105ba15a721f3ec1a57694fa304 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 31 Jan 2025 08:35:20 +0100 Subject: [PATCH 19/59] plan once use todays date --- packages/helpermodules/abstract_plans.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/helpermodules/abstract_plans.py b/packages/helpermodules/abstract_plans.py index fced1bc009..99271cf6dd 100644 --- a/packages/helpermodules/abstract_plans.py +++ b/packages/helpermodules/abstract_plans.py @@ -1,9 +1,10 @@ from dataclasses import dataclass, field +import datetime from typing import List, Optional def once_factory() -> List: - return ["2021-11-01", "2021-11-05"] # ToDo: aktuelles Datum verwenden + return [datetime.datetime.today().strftime("%Y%m%d"), datetime.datetime.today().strftime("%Y%m%d")] def weekly_factory() -> List: From 1d22c54147e50fc0f75619c2678e3b38c10f2b40 Mon Sep 17 00:00:00 2001 From: Lutz Bender Date: Tue, 11 Feb 2025 13:37:05 +0100 Subject: [PATCH 20/59] standard-legacy web-theme --- .../standard_legacy/web/helperFunctions.js | 9 +- .../web_themes/standard_legacy/web/index.html | 459 +++++++++++++----- .../standard_legacy/web/processAllMqttMsg.js | 140 +++++- 3 files changed, 460 insertions(+), 148 deletions(-) diff --git a/packages/modules/web_themes/standard_legacy/web/helperFunctions.js b/packages/modules/web_themes/standard_legacy/web/helperFunctions.js index 30802a32ea..00dcfc051b 100644 --- a/packages/modules/web_themes/standard_legacy/web/helperFunctions.js +++ b/packages/modules/web_themes/standard_legacy/web/helperFunctions.js @@ -139,7 +139,12 @@ function setToggleBtnGroup(groupId, option) { chargemodeOptionsShowHide(btnGroup, option); } if (btnGroup.hasClass('charge-point-instant-charge-limit-selected')) { - chargemodeLimitOptionsShowHide(btnGroup, option); + chargemodeLimitOptionsShowHide(btnGroup, 'instant', option); + } + if (btnGroup.hasClass('charge-point-pv-charge-limit-selected')) { + chargemodeLimitOptionsShowHide(btnGroup, 'pv', option); + } + if (btnGroup.hasClass('charge-point-eco-charge-limit-selected')) { + chargemodeLimitOptionsShowHide(btnGroup, 'eco', option); } - } diff --git a/packages/modules/web_themes/standard_legacy/web/index.html b/packages/modules/web_themes/standard_legacy/web/index.html index 36132c1147..426cc4c3ee 100644 --- a/packages/modules/web_themes/standard_legacy/web/index.html +++ b/packages/modules/web_themes/standard_legacy/web/index.html @@ -457,12 +457,14 @@
+
--
+
@@ -477,6 +479,7 @@

+
+
@@ -498,22 +502,23 @@ data-toggle="buttons" data-name="chargemode" data-topic="openWB/set/vehicle/template/charge_template//chargemode/selected">
+
@@ -526,97 +531,66 @@ data-style="w-100">
-
-
-
-
+
+ +
+
+
+ + Zeitladen +
+
+ +
+
+
+
- - Strompreisbasiert Laden +

Termine Zeitladen

-
-
-
- -
-
- -
-
+
+ +
+
+
+ Es wurden noch keine Zeitpläne eingerichtet! +
+
+
+ -- +
+
+ + + + -- + + + + -- + + -- + -- +
-
-
-
- - Zeitladen -
-
- -
-
-
-
-
-

Termine Zeitladen

-
-
- -
-
-
- Es wurden noch keine Zeitpläne eingerichtet! -
-
-
- -- -
-
- - - - -- - - - - -- - - -- - - -
- -
-
-

+
-

Einstellungen für "Sofortladen"

+

Einstellungen für "Sofort"

Stromstärke @@ -660,25 +634,41 @@

Einstellungen für "Sofortladen"

+
+
+ +
+
+ + +
+
-
+
@@ -689,10 +679,10 @@

Einstellungen für "Sofortladen"

-
@@ -708,11 +698,11 @@

Einstellungen für "Sofortladen"

-
@@ -720,6 +710,7 @@

Einstellungen für "Sofortladen"

+

Einstellungen für "PV"

@@ -765,6 +756,83 @@

Einstellungen für "PV"

+
+
+ +
+
+ + + +
+
+
+
+ +
+
+ + + +
+
+
+
+
+ SoC-Limit für das Fahrzeug +
+
+
+
+ +
+ +
+
+
+
+
+ Energie-Limit +
+
+
+
+ +
+ +
+
+
+
Mindest-SoC für das Fahrzeug @@ -827,24 +895,20 @@

Einstellungen für "PV"

-
+
- SoC-Limit für das Fahrzeug +
-
-
-
- -
- -
+
+ +
@@ -860,10 +924,11 @@

Einstellungen für "PV"

+
-

Termine Zielladen

+

Einstellungen für "Ziel"

- +
+
+ +
+

Einstellungen für "Eco"

+
+
+ Minimaler Dauerstrom unter der Preisgrenze +
+
+
+
+ +
+ +
+
+
+
+
+
+ Minimale Dauerleistung unter der Preisgrenze +
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+
+ + + +
+
+
+
+ +
+
+ + + +
+
+
+
+
+ SoC-Limit für das Fahrzeug +
+
+
+
+ +
+ +
+
+
+
+
+ Energie-Limit +
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+ + Preisgrenze für strompreisbasiertes Laden +
+
+ +
+
+
@@ -1068,7 +1275,7 @@
@@ -745,7 +745,7 @@

Einstellungen für "PV"

class="charge-point-pv-charge-dc-min-current form-control-range rangeInput" id="minDcCurrentPvCpT" data-transformation='{ "in": "( * 3 * 230) / 1000", "out": "( * 1000) / 230 / 3" }' - data-topic="openWB/set/vehicle/template/charge_template//chargemode/pv_charging/dc_min_current" + data-topic="openWB/set/chargepoint//set/charge_template/chargemode/pv_charging/dc_min_current" min="0" max="300" step="1">