diff --git a/data/config/mosquitto/openwb_local.conf b/data/config/mosquitto/openwb_local.conf index 13fbd9ec8c..0a3bc92c65 100644 --- a/data/config/mosquitto/openwb_local.conf +++ b/data/config/mosquitto/openwb_local.conf @@ -1,4 +1,4 @@ -# openwb-version:16 +# openwb-version:17 listener 1886 localhost allow_anonymous true @@ -22,6 +22,8 @@ topic openWB/chargepoint/+/set/phases_to_use out 2 topic openWB/chargepoint/+/set/manual_lock out 2 topic openWB/chargepoint/+/set/autolock_state out 2 topic openWB/chargepoint/+/set/rfid out 2 +topic openWB/chargepoint/+/set/charge_template out 2 +topic openWB/chargepoint/+/set/charge_template/# out 2 topic openWB/chargepoint/+/get/# out 2 topic openWB/chargepoint/+/config/# out 2 topic openWB/chargepoint/template/# out 2 diff --git a/docs/Ladeprofile.md b/docs/Ladeprofile.md index 1238da39ad..f0231162f1 100644 --- a/docs/Ladeprofile.md +++ b/docs/Ladeprofile.md @@ -3,3 +3,10 @@ _Einstellungen -> Konfiguration -> Fahrzeuge -> Lade-Profile_ Unter den Lade-Profilen werden die Einstellungen für das Ladeprofil verwaltet. Die Einstellungen auf der Hauptseite werden aus diesem Profil geladen und dorthin geschrieben. Ist nur ein Fahrzeug vorhanden, so wird in den meisten Fällen nur das Standard-Ladeprofil benötigt. Ausgenommen hiervon ist, wenn per RFID-Tag Ladevorgaben ausgewählt werden. In den fahrzeugspezifischen Einstellungen wird ein Ladeprofil einem Fahrzeug zugeordnet. Werden zwei Fahrzeuge geladen, empfiehlt es sich dazu ein zweites Ladeprofil anzulegen. + +### Temporäre Ladeprofile (ab Version 2.1.7) +Anpassungen am Ladeprofil, die über die Hauptseite (Web-Themes) oder ein Display (Display-Themes) vorgenommen werden, sind temporär. Die Lade-Profile müssen direkt in den Einstellungen bearbeitet werden. +Die temporären Einstellungen werden mit dem Ladeprofil aus den Einstellungen überschrieben, wenn +... abgesteckt wird. +... das Fahrzeug gewechselt wird. Das Lade-Profil des neuen Fahrzeugs wird geladen. +... das Ladeprofil geändert wird und kein Fahrzeug angesteckt ist. Ist ein Fahrzeug angesteckt, gelten die temporären Einstellungen bis zum Abstecken und werden dann durch das Ladeprofil überschrieben. \ No newline at end of file diff --git a/packages/control/algorithm/additional_current_test.py b/packages/control/algorithm/additional_current_test.py index 89c552aeff..4a8904a082 100644 --- a/packages/control/algorithm/additional_current_test.py +++ b/packages/control/algorithm/additional_current_test.py @@ -30,7 +30,7 @@ def test_set_loadmangement_message(set_current, limit, expected_msg, monkeypatch): # setup ev = Ev(0) - ev.charge_template = ChargeTemplate(0) + ev.charge_template = ChargeTemplate() cp1 = Chargepoint(1, None) cp1.data = ChargepointData(set=Set(current=set_current), control_parameter=ControlParameter(required_currents=[8]*3)) diff --git a/packages/control/algorithm/algorithm.py b/packages/control/algorithm/algorithm.py index 6c4af1008f..63192124eb 100644 --- a/packages/control/algorithm/algorithm.py +++ b/packages/control/algorithm/algorithm.py @@ -62,6 +62,7 @@ def _check_auto_phase_switch_delay(self) -> None: # wurden, wieder zurückgegeben. log.debug(f"Ladepunkt {cp.num}: Prüfen, ob Phasenumschaltung durchgeführt werden soll.") phases, current, message = charging_ev.auto_phase_switch( + cp.data.set.charge_template, cp.data.control_parameter, cp.num, cp.data.get.currents, diff --git a/packages/control/algorithm/integration_test/pv_charging_test.py b/packages/control/algorithm/integration_test/pv_charging_test.py index 28eb69842e..31bbbd414f 100644 --- a/packages/control/algorithm/integration_test/pv_charging_test.py +++ b/packages/control/algorithm/integration_test/pv_charging_test.py @@ -228,9 +228,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(surplus_controlled, "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 + data.data.cp_data["cp3"].data.set.charge_template.data.chargemode.pv_charging.phases_to_use = 1 + data.data.cp_data["cp4"].data.set.charge_template.data.chargemode.pv_charging.phases_to_use = 1 + data.data.cp_data["cp5"].data.set.charge_template.data.chargemode.pv_charging.phases_to_use = 1 # execution Algorithm().calc_current() diff --git a/packages/control/algorithm/surplus_controlled.py b/packages/control/algorithm/surplus_controlled.py index 51e0aaa1c7..725b2315cb 100644 --- a/packages/control/algorithm/surplus_controlled.py +++ b/packages/control/algorithm/surplus_controlled.py @@ -96,9 +96,9 @@ def _set_loadmangement_message(self, # tested def filter_by_feed_in_limit(self, chargepoints: List[Chargepoint]) -> Tuple[List[Chargepoint], List[Chargepoint]]: - cp_with_feed_in = list(filter(lambda cp: cp.data.set.charging_ev_data.charge_template.data.chargemode. + cp_with_feed_in = list(filter(lambda cp: cp.data.set.charge_template.data.chargemode. pv_charging.feed_in_limit is True, chargepoints)) - cp_without_feed_in = list(filter(lambda cp: cp.data.set.charging_ev_data.charge_template.data.chargemode. + cp_without_feed_in = list(filter(lambda cp: cp.data.set.charge_template.data.chargemode. pv_charging.feed_in_limit is False, chargepoints)) return cp_with_feed_in, cp_without_feed_in @@ -110,7 +110,7 @@ def _limit_adjust_current(self, chargepoint: Chargepoint, new_current: float) -> MAX_CURRENT = 30 msg = None nominal_difference = chargepoint.data.set.charging_ev_data.ev_template.data.nominal_difference - if chargepoint.data.set.charging_ev_data.chargemode_changed or chargepoint.data.get.charge_state is False: + if chargepoint.chargemode_changed or chargepoint.data.get.charge_state is False: return new_current else: # Um max. +/- 5A pro Zyklus regeln @@ -153,7 +153,7 @@ def check_submode_pv_charging(self) -> None: def phase_switch_necessary() -> bool: return cp.cp_ev_chargemode_support_phase_switch() and cp.data.get.phases_in_use != 1 control_parameter = cp.data.control_parameter - if cp.data.set.charging_ev_data.chargemode_changed or cp.data.set.charging_ev_data.submode_changed: + if cp.chargemode_changed or cp.submode_changed: if control_parameter.state == ChargepointState.CHARGING_ALLOWED: if (cp.data.set.charging_ev_data.ev_template.data.prevent_charge_stop is False and phase_switch_necessary() is False): diff --git a/packages/control/algorithm/surplus_controlled_test.py b/packages/control/algorithm/surplus_controlled_test.py index 6e50210534..d9eb6411f2 100644 --- a/packages/control/algorithm/surplus_controlled_test.py +++ b/packages/control/algorithm/surplus_controlled_test.py @@ -11,7 +11,6 @@ from control.chargepoint.chargepoint_data import Get, Set from control.chargepoint.chargepoint_template import CpTemplate from control.chargepoint.control_parameter import ControlParameter -from control.ev.charge_template import ChargeTemplate from control.ev.ev import Ev @@ -40,10 +39,8 @@ def test_filter_by_feed_in_limit(feed_in_limit_1: bool, expected_sorted: int): # setup def setup_cp(cp: Chargepoint, feed_in_limit: bool) -> Chargepoint: - ev = Ev(0) - ev.charge_template = ChargeTemplate(0) - ev.charge_template.data.chargemode.pv_charging.feed_in_limit = feed_in_limit - cp.data = ChargepointData(set=Set(charging_ev_data=ev)) + cp.data = ChargepointData() + cp.data.set.charge_template.data.chargemode.pv_charging.feed_in_limit = feed_in_limit return cp cp1 = setup_cp(mock_cp1, feed_in_limit_1) diff --git a/packages/control/auto_phase_switch_test.py b/packages/control/auto_phase_switch_test.py index 25531d2e96..b31e2013d5 100644 --- a/packages/control/auto_phase_switch_test.py +++ b/packages/control/auto_phase_switch_test.py @@ -5,6 +5,7 @@ from control.chargepoint.control_parameter import ControlParameter from control.counter import Counter, CounterData, Set +from control.ev.charge_template import ChargeTemplate from control.pv_all import PvAll from control.bat_all import BatAll from control.general import General @@ -131,7 +132,8 @@ def test_auto_phase_switch(monkeypatch, vehicle: Ev, params: Params): control_parameter.state = params.state # execution - phases_to_use, current, message = vehicle.auto_phase_switch(control_parameter, + phases_to_use, current, message = vehicle.auto_phase_switch(ChargeTemplate(), + control_parameter, 0, params.get_currents, params.get_power, diff --git a/packages/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index 6a20efeb2a..31d54ea121 100644 --- a/packages/control/chargepoint/chargepoint.py +++ b/packages/control/chargepoint/chargepoint.py @@ -14,6 +14,7 @@ Tag-Liste: Tags, mit denen der Ladepunkt freigeschaltet werden kann. Ist diese leer, kann mit jedem Tag der Ladepunkt freigeschaltet werden. """ +import copy from dataclasses import asdict import dataclasses import logging @@ -29,9 +30,11 @@ from control.chargepoint.control_parameter import ControlParameter, control_parameter_factory from control.chargepoint.charging_type import ChargingType from control.chargepoint.rfid import ChargepointRfidMixin +from control.ev.charge_template import ChargeTemplate from control.ev.ev import Ev from control import phase_switch from control.chargepoint.chargepoint_state import CHARGING_STATES, ChargepointState +from helpermodules.broker import InternalBrokerClient from helpermodules.phase_mapping import convert_single_evu_phase_to_cp_phase from helpermodules.pub import Pub from helpermodules import timecheck @@ -70,6 +73,8 @@ def __init__(self, index: int, event: Optional[threading.Event]): self.template: CpTemplate = None self.chargepoint_module: AbstractChargepoint = None self.num = index + self.chargemode_changed = False + self.submode_changed = False # bestehende Daten auf dem Broker nicht zurücksetzen, daher nicht veröffentlichen self.data: ChargepointData = ChargepointData() self.data.set_event(event) @@ -226,7 +231,7 @@ def _process_charge_stop(self) -> None: if not self.data.get.plug_state: self.data.control_parameter = control_parameter_factory() # Standardprofil nach Abstecken laden - if data.data.ev_data["ev"+str(self.data.set.charging_ev_prev)].charge_template.data.load_default: + if self.data.set.charge_template.data.load_default: self.data.config.ev = 0 Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/config/ev", 0) # Ladepunkt nach Abstecken sperren @@ -235,6 +240,8 @@ def _process_charge_stop(self) -> None: Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/manual_lock", True) log.debug("/set/manual_lock True") # Ev wurde noch nicht aktualisiert. + # Ladeprofil aus den Einstellungen laden. + self.update_charge_template(self.data.set.charging_ev_data.charge_template) chargelog.save_and_reset_data(self, data.data.ev_data["ev"+str(self.data.set.charging_ev_prev)]) self.data.set.charging_ev_prev = -1 Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/charging_ev_prev", @@ -280,8 +287,8 @@ def set_control_parameter(self, submode: str, required_current: float): self.data.control_parameter.chargemode = Chargemode.TIME_CHARGING else: self.data.control_parameter.chargemode = Chargemode( - self.data.set.charging_ev_data.charge_template.data.chargemode.selected) - self.data.control_parameter.prio = self.data.set.charging_ev_data.charge_template.data.prio + self.data.set.charge_template.data.chargemode.selected) + self.data.control_parameter.prio = self.data.set.charge_template.data.prio self.data.control_parameter.required_current = required_current if self.template.data.charging_type == ChargingType.AC.value: self.data.control_parameter.min_current = self.data.set.charging_ev_data.ev_template.data.min_current @@ -302,8 +309,10 @@ def _set_values_at_start(self): def remember_previous_values(self): self.data.set.plug_state_prev = self.data.get.plug_state self.data.set.current_prev = self.data.set.current + self.data.set.ev_prev = self.data.config.ev Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/plug_state_prev", self.data.set.plug_state_prev) Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/current_prev", self.data.set.current_prev) + Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/ev_prev", self.data.set.ev_prev) def reset_log_data_chargemode_switch(self) -> None: reset_log = Log() @@ -611,6 +620,18 @@ def set_timestamp_charge_start(self): elif self.data.set.current == 0: self.data.control_parameter.timestamp_charge_start = None + def set_chargemode_changed(self, submode: str) -> None: + if ((submode == "time_charging" and self.data.control_parameter.chargemode != "time_charging") or + (submode != "time_charging" and + self.data.control_parameter.chargemode != self.data.set.charge_template.data.chargemode.selected)): + self.chargemode_changed = True + log.debug("Änderung des Lademodus") + else: + self.chargemode_changed = False + + def set_submode_changed(self, submode: str) -> None: + self.submode_changed = (submode != self.data.control_parameter.submode) + def update_ev(self, ev_list: Dict[str, Ev]) -> None: self._validate_rfid() charging_possible = self.is_charging_possible()[0] @@ -623,6 +644,8 @@ def update_ev(self, ev_list: Dict[str, Ev]) -> None: else: vehicle = -1 self._pub_configured_ev(ev_list) + if self.data.config.ev != self.data.set.ev_prev: + self.update_charge_template(ev_list[f"ev{self.data.config.ev}"].charge_template) def update(self, ev_list: Dict[str, Ev]) -> None: try: @@ -641,6 +664,7 @@ def update(self, ev_list: Dict[str, Ev]) -> None: charging_ev = self._get_charging_ev(vehicle, ev_list) max_phase_hw = self.get_max_phase_hw() state, message_ev, submode, required_current, phases = charging_ev.get_required_current( + self.data.set.charge_template, self.data.control_parameter, max_phase_hw, self.cp_ev_support_phase_switch(), @@ -655,20 +679,20 @@ def update(self, ev_list: Dict[str, Ev]) -> None: required_current = self.check_min_max_current( required_current, self.data.control_parameter.phases) required_current = self.chargepoint_module.add_conversion_loss_to_current(required_current) - charging_ev.set_chargemode_changed(self.data.control_parameter, submode) - charging_ev.set_submode_changed(self.data.control_parameter, submode) + self.set_chargemode_changed(submode) + self.set_submode_changed(submode) self.set_control_parameter(submode, required_current) self.set_required_currents(required_current) self.check_phase_switch_completed() - if charging_ev.chargemode_changed or charging_ev.submode_changed: + if self.chargemode_changed or self.submode_changed: data.data.counter_all_data.get_evu_counter().reset_switch_on_off( self, charging_ev) charging_ev.reset_phase_switch(self.data.control_parameter) message = message_ev if message_ev else message # Ein Eintrag muss nur erstellt werden, wenn vorher schon geladen wurde und auch danach noch # geladen werden soll. - if charging_ev.chargemode_changed and self.data.set.log.imported_since_mode_switch != 0 and state: + if self.chargemode_changed and self.data.set.log.imported_since_mode_switch != 0 and state: chargelog.save_interim_data(self, charging_ev) # Wenn die Nachrichten gesendet wurden, EV wieder löschen, wenn das EV im Algorithmus nicht @@ -684,7 +708,7 @@ def update(self, ev_list: Dict[str, Ev]) -> None: str(self.num)+"/set/charging_ev", -1) log.debug(f'LP {self.num}, EV: {self.data.set.charging_ev_data.data.name}' f' (EV-Nr.{vehicle}): Lademodus ' - f'{charging_ev.charge_template.data.chargemode.selected}, Submodus: ' + f'{self.data.set.charge_template.data.chargemode.selected}, Submodus: ' f'{self.data.control_parameter.submode}') else: if (self.data.control_parameter.state == ChargepointState.SWITCH_ON_DELAY and @@ -694,10 +718,10 @@ def update(self, ev_list: Dict[str, Ev]) -> None: log.info( f"LP {self.num}, EV: {self.data.set.charging_ev_data.data.name} (EV-Nr.{vehicle}): " f"Theoretisch benötigter Strom {required_current}A, Lademodus " - f"{charging_ev.charge_template.data.chargemode.selected}, Submodus: " + f"{self.data.set.charge_template.data.chargemode.selected}, Submodus: " f"{self.data.control_parameter.submode}, Phasen: " f"{self.data.control_parameter.phases}" - f", Priorität: {charging_ev.charge_template.data.prio}" + f", Priorität: {self.data.control_parameter.prio}" f", max. Ist-Strom: {max(self.data.get.currents)}") except Exception: log.exception("Fehler im Prepare-Modul für Ladepunkt "+str(self.num)) @@ -706,8 +730,13 @@ def update(self, ev_list: Dict[str, Ev]) -> None: self._process_charge_stop() if vehicle != -1: self._pub_connected_vehicle(ev_list[f"ev{vehicle}"]) + if self.data.set.charge_template.data.id != ev_list[f"ev{vehicle}"].charge_template.data.id: + self.update_charge_template(ev_list[f"ev{vehicle}"].charge_template) else: self._pub_configured_ev(ev_list) + if self.data.set.charge_template.data.id != ev_list[ + f"ev{self.data.config.ev}"].charge_template.data.id: + self.update_charge_template(ev_list[f"ev{self.data.config.ev}"].charge_template) # OCPP Start Transaction nach Anstecken if ((self.data.get.plug_state and self.data.set.plug_state_prev is False) or (self.data.set.ocpp_transaction_id is None and self.data.get.charge_state)): @@ -750,6 +779,9 @@ def _get_charging_ev(self, vehicle: int, ev_list: Dict[str, Ev]) -> Ev: if self.data.set.charging_ev != vehicle and self.data.set.charging_ev_prev != vehicle: Pub().pub(f"openWB/set/vehicle/{charging_ev.num}/get/force_soc_update", True) log.debug("SoC nach EV-Wechsel") + self.update_charge_template(charging_ev.charge_template) + if self.data.set.charge_template.data.id != charging_ev.charge_template.data.id: + self.update_charge_template(charging_ev.charge_template) self.data.set.charging_ev_data = charging_ev self.data.set.charging_ev = vehicle Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/charging_ev", vehicle) @@ -757,6 +789,32 @@ def _get_charging_ev(self, vehicle: int, ev_list: Dict[str, Ev]) -> Ev: Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/charging_ev_prev", vehicle) return charging_ev + def update_charge_template(self, charge_template: ChargeTemplate) -> None: + # evtl noch vorhandene, aber in den Einstellungen gelöschte Pläne entfernen + def on_connect(client, userdata, flags, rc): + client.subscribe(f'openWB/chargepoint/{self.num}/set/charge_template/#', 2) + + def __get_payload(client, userdata, msg): + received_topics.append(msg.topic) + if self.data.get.plug_state: + return + received_topics = [] + InternalBrokerClient("processBrokerBranch", on_connect, __get_payload).start_finite_loop() + for topic in received_topics: + Pub().pub(topic, "") + self.data.set.charge_template = copy.deepcopy(charge_template) + pub_template = copy.deepcopy(self.data.set.charge_template.data) + pub_template = dataclasses.asdict(pub_template) + pub_template["chargemode"]["scheduled_charging"]["plans"].clear() + pub_template["time_charging"]["plans"].clear() + Pub().pub(f"openWB/set/chargepoint/{self.num}/set/charge_template", pub_template) + for id, plan in self.data.set.charge_template.data.time_charging.plans.items(): + Pub().pub(f"openWB/set/chargepoint/{self.num}/set/charge_template/time_charging/plans/{id}", + dataclasses.asdict(plan)) + for id, plan in self.data.set.charge_template.data.chargemode.scheduled_charging.plans.items(): + Pub().pub(f"openWB/set/chargepoint/{self.num}/set/charge_template/chargemode/scheduled_charging/plans/{id}", + dataclasses.asdict(plan)) + def _pub_connected_vehicle(self, vehicle: Ev): """ published die Daten, die zur Anzeige auf der Hauptseite benötigt werden. @@ -780,40 +838,38 @@ def _pub_connected_vehicle(self, vehicle: Ev): soc_obj.range = vehicle.data.get.range info_obj = ConnectedInfo(id=vehicle.num, name=vehicle.data.name) - if (vehicle.charge_template.data.chargemode.selected == "time_charging" or - vehicle.charge_template.data.chargemode.selected == "scheduled_charging"): + if (self.data.set.charge_template.data.chargemode.selected == "time_charging" or + self.data.set.charge_template.data.chargemode.selected == "scheduled_charging"): current_plan = self.data.control_parameter.current_plan else: current_plan = None config_obj = ConnectedConfig( - charge_template=vehicle.charge_template.ct_num, - ev_template=vehicle.ev_template.et_num, - chargemode=vehicle.charge_template.data.chargemode.selected, - priority=vehicle.charge_template.data.prio, + charge_template=self.data.set.charge_template.data.id, + ev_template=vehicle.ev_template.data.id, + chargemode=self.data.set.charge_template.data.chargemode.selected, + priority=self.data.set.charge_template.data.prio, current_plan=current_plan, average_consumption=vehicle.ev_template.data.average_consump, time_charging_in_use=True if (self.data.control_parameter.submode == "time_charging") else False) if soc_obj != self.data.get.connected_vehicle.soc: - Pub().pub("openWB/chargepoint/"+str(self.num) + - "/get/connected_vehicle/soc", dataclasses.asdict(soc_obj)) + Pub().pub(f"openWB/chargepoint/{self.num}/get/connected_vehicle/soc", dataclasses.asdict(soc_obj)) if info_obj != self.data.get.connected_vehicle.info: - Pub().pub("openWB/chargepoint/"+str(self.num) + - "/get/connected_vehicle/info", dataclasses.asdict(info_obj)) + Pub().pub(f"openWB/chargepoint/{self.num}/get/connected_vehicle/info", dataclasses.asdict(info_obj)) if config_obj != self.data.get.connected_vehicle.config: - Pub().pub("openWB/chargepoint/"+str(self.num) + - "/get/connected_vehicle/config", dataclasses.asdict(config_obj)) + Pub().pub(f"openWB/chargepoint/{self.num}/get/connected_vehicle/config", + dataclasses.asdict(config_obj)) except Exception: log.exception("Fehler im Prepare-Modul") 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 - self.data.set.charging_ev_data.charge_template.data.chargemode.pv_charging.phases_to_use == 0) + self.data.set.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 - self.data.set.charging_ev_data.charge_template.data.chargemode.scheduled_charging.plans[ + self.data.set.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 diff --git a/packages/control/chargepoint/chargepoint_data.py b/packages/control/chargepoint/chargepoint_data.py index 9d2f9bb02c..c4fa538a52 100644 --- a/packages/control/chargepoint/chargepoint_data.py +++ b/packages/control/chargepoint/chargepoint_data.py @@ -4,6 +4,7 @@ from control.chargepoint.chargepoint_template import CpTemplate from control.chargepoint.control_parameter import ControlParameter, control_parameter_factory +from control.ev.charge_template import ChargeTemplate from control.ev.ev import Ev from dataclass_utils.factories import currents_list_factory, empty_dict_factory, voltages_list_factory from helpermodules.constants import NO_ERROR @@ -117,6 +118,10 @@ class Get: voltages: List[float] = field(default_factory=voltages_list_factory) +def charge_template_factory() -> ChargeTemplate: + return ChargeTemplate() + + def ev_factory() -> Ev: return Ev(0) @@ -129,8 +134,10 @@ def log_factory() -> Log: class Set: charging_ev: int = -1 charging_ev_prev: int = -1 + charge_template: ChargeTemplate = field(default_factory=charge_template_factory) current: float = 0 energy_to_charge: float = 0 + ev_prev: int = 0 loadmanagement_available: bool = True log: Log = field(default_factory=log_factory) manual_lock: bool = False diff --git a/packages/control/counter.py b/packages/control/counter.py index c61cfdd2d7..5a60dc9fae 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -264,7 +264,7 @@ def calc_switch_on_power(self, chargepoint: Chargepoint) -> Tuple[float, float]: control_parameter = chargepoint.data.control_parameter pv_config = data.data.general_data.data.chargemode_config.pv_charging - if chargepoint.data.set.charging_ev_data.charge_template.data.chargemode.pv_charging.feed_in_limit: + if chargepoint.data.set.charge_template.data.chargemode.pv_charging.feed_in_limit: threshold = pv_config.feed_in_yield else: threshold = pv_config.switch_on_threshold*control_parameter.phases @@ -274,7 +274,7 @@ def switch_on_threshold_reached(self, chargepoint: Chargepoint) -> None: try: message = None control_parameter = chargepoint.data.control_parameter - feed_in_limit = chargepoint.data.set.charging_ev_data.charge_template.data.chargemode.pv_charging.\ + feed_in_limit = chargepoint.data.set.charge_template.data.chargemode.pv_charging.\ feed_in_limit pv_config = data.data.general_data.data.chargemode_config.pv_charging timestamp_switch_on_off = control_parameter.timestamp_switch_on_off @@ -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.CHARGING_ALLOWED - if charging_ev_data.charge_template.data.chargemode.pv_charging.feed_in_limit: + if chargepoint.data.set.charge_template.data.chargemode.pv_charging.feed_in_limit: feed_in_yield = pv_config.feed_in_yield else: feed_in_yield = 0 ev_template = charging_ev_data.ev_template max_phases_power = ev_template.data.min_current * ev_template.data.max_phases * 230 if (control_parameter.submode == "pv_charging" and - charging_ev_data.charge_template.data.chargemode.pv_charging.phases_to_use == 0 and + chargepoint.data.set.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 @@ -385,7 +385,7 @@ def switch_off_check_timer(self, chargepoint: Chargepoint) -> None: def calc_switch_off_threshold(self, chargepoint: Chargepoint) -> Tuple[float, float]: pv_config = data.data.general_data.data.chargemode_config.pv_charging control_parameter = chargepoint.data.control_parameter - if chargepoint.data.set.charging_ev_data.charge_template.data.chargemode.pv_charging.feed_in_limit: + if chargepoint.data.set.charge_template.data.chargemode.pv_charging.feed_in_limit: # Der EVU-Überschuss muss ggf um die Einspeisegrenze bereinigt werden. # Wnn die Leistung nicht Einspeisegrenze + Einschaltschwelle erreicht, darf die Ladung nicht pulsieren. # Abschaltschwelle um Einschaltschwelle reduzieren. diff --git a/packages/control/counter_test.py b/packages/control/counter_test.py index 4b10d1335f..4b3e59466a 100644 --- a/packages/control/counter_test.py +++ b/packages/control/counter_test.py @@ -140,7 +140,7 @@ def test_switch_on_threshold_reached(params: Params, caplog, general_data_fixtur cp.data.control_parameter.phases = 1 cp.data.control_parameter.state = params.state cp.data.control_parameter.timestamp_switch_on_off = params.timestamp_switch_on_off - ev.data.charge_template = ChargeTemplate(0) + ev.data.charge_template = ChargeTemplate() ev.data.charge_template.data.chargemode.pv_charging.feed_in_limit = params.feed_in_limit cp.data.set.charging_ev_data = ev mock_calc_switch_on_power = Mock(return_value=[params.surplus, params.threshold]) diff --git a/packages/control/ev/charge_template.py b/packages/control/ev/charge_template.py index 988637ce73..c17131db5f 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -1,14 +1,14 @@ from dataclasses import asdict, dataclass, field import logging import traceback -from typing import Dict, Optional, Tuple +from typing import Optional, Tuple 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 +from helpermodules.abstract_plans import Limit, limit_factory, ScheduledChargingPlan from helpermodules import timecheck log = logging.getLogger(__name__) @@ -29,15 +29,15 @@ def get_charge_template_default() -> dict: @dataclass class ScheduledCharging: - plans: Dict[int, ScheduledChargingPlan] = field(default_factory=empty_dict_factory, metadata={ - "topic": ""}) + plans: dict = field(default_factory=empty_dict_factory, metadata={ + "topic": ""}) # Dict[int,ScheduledChargingPlan] wird bei der dict to dataclass Konvertierung nicht unterstützt @dataclass class TimeCharging: active: bool = False - plans: Dict[int, TimeChargingPlan] = field(default_factory=empty_dict_factory, metadata={ - "topic": ""}) + plans: dict = field(default_factory=empty_dict_factory, metadata={ + "topic": ""}) # Dict[int, TimeChargingPlan] wird bei der dict to dataclass Konvertierung nicht unterstützt @dataclass @@ -105,6 +105,7 @@ def chargemode_factory() -> Chargemode: @dataclass class ChargeTemplateData: + id: int = 0 name: str = "Lade-Profil" prio: bool = False load_default: bool = False @@ -129,7 +130,6 @@ class SelectedPlan: class ChargeTemplate: """ Klasse der Lade-Profile """ - ct_num: int data: ChargeTemplateData = field(default_factory=charge_template_data_factory, metadata={ "topic": ""}) @@ -180,7 +180,7 @@ def time_charging(self, sub_mode = "stop" return current, sub_mode, message, id, phases except Exception: - log.exception("Fehler im ev-Modul "+str(self.ct_num)) + log.exception("Fehler im ev-Modul "+str(self.data.id)) return (0, "stop", "Keine Ladung, da da ein interner Fehler aufgetreten ist: "+traceback.format_exc(), None, 0) @@ -214,7 +214,7 @@ def instant_charging(self, message = self.AMOUNT_REACHED return current, sub_mode, message, phases except Exception: - log.exception("Fehler im ev-Modul "+str(self.ct_num)) + log.exception("Fehler im ev-Modul "+str(self.data.id)) 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 " @@ -300,7 +300,7 @@ def eco_charging(self, current = min_current return current, sub_mode, message, phases except Exception: - log.exception("Fehler im ev-Modul "+str(self.ct_num)) + log.exception("Fehler im ev-Modul "+str(self.data.id)) return 0, "stop", "Keine Ladung, da ein interner Fehler aufgetreten ist: "+traceback.format_exc(), 0 def scheduled_charging_recent_plan(self, diff --git a/packages/control/ev/charge_template_test.py b/packages/control/ev/charge_template_test.py index 6592d9413d..0d37b83154 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -8,7 +8,7 @@ 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 +from control.ev.charge_template import ChargeTemplate from control.ev.ev_template import EvTemplate, EvTemplateData from control.general import General from helpermodules import timecheck @@ -59,7 +59,7 @@ def test_time_charging(plans: Dict[int, TimeChargingPlan], soc: float, used_amou expected: Tuple[int, str, Optional[str], Optional[str]], monkeypatch): # setup - ct = ChargeTemplate(0) + ct = ChargeTemplate() ct.data.time_charging.plans = plans check_plans_timeframe_mock = Mock(return_value=plan_found) monkeypatch.setattr(timecheck, "check_plans_timeframe", check_plans_timeframe_mock) @@ -86,7 +86,8 @@ def test_time_charging(plans: Dict[int, TimeChargingPlan], soc: float, used_amou def test_instant_charging(selected: str, current_soc: float, used_amount: float, expected: Tuple[int, str, Optional[str]]): # setup - ct = ChargeTemplate(0) + data.data.optional_data.data.et.active = False + ct = ChargeTemplate() ct.data.chargemode.instant_charging.limit.selected = selected # execution @@ -111,7 +112,7 @@ def test_instant_charging(selected: str, current_soc: float, used_amount: 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) + ct = ChargeTemplate() 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 @@ -143,7 +144,7 @@ def test_calc_remaining_time(phases_to_use, phase_switch_supported, expected, monkeypatch): # setup - ct = ChargeTemplate(0) + ct = ChargeTemplate() plan = ScheduledChargingPlan(phases_to_use=phases_to_use) calculate_duration_mock = Mock(side_effect=calc_duration) monkeypatch.setattr(ChargeTemplate, "_calculate_duration", calculate_duration_mock) @@ -166,7 +167,7 @@ def test_calc_remaining_time(phases_to_use, ]) def test_calculate_duration(selected: str, phases: int, expected_duration: float, expected_missing_amount: float): # setup - ct = ChargeTemplate(0) + ct = ChargeTemplate() plan = ScheduledChargingPlan() plan.limit.selected = selected # execution @@ -193,7 +194,7 @@ def test_sscheduled_charging_recent_plan(end_time_mock, 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) + ct = ChargeTemplate() 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")) plan_mock_2 = Mock(spec=ScheduledChargingPlan, active=True, current=14, id=2, limit=Limit(selected="amount")) @@ -246,7 +247,7 @@ def test_scheduled_charging_calc_current(plan_data: SelectedPlan, selected: str, expected: Tuple[float, str, str, int]): # setup - ct = ChargeTemplate(0) + ct = ChargeTemplate() plan = ScheduledChargingPlan(active=True, id=0) plan.limit.selected = selected # json verwandelt Keys in strings @@ -263,7 +264,7 @@ def test_scheduled_charging_calc_current(plan_data: SelectedPlan, def test_scheduled_charging_calc_current_no_plans(): # setup - ct = ChargeTemplate(0) + ct = ChargeTemplate() # execution ret = ct.scheduled_charging_calc_current(None, 63, 5, 3, 6, 0, ChargingType.AC.value, EvTemplate()) @@ -280,7 +281,7 @@ def test_scheduled_charging_calc_current_no_plans(): ]) def test_scheduled_charging_calc_current_electricity_tariff(loading_hour, expected, monkeypatch): # setup - ct = ChargeTemplate(0) + ct = ChargeTemplate() plan = ScheduledChargingPlan(active=True) plan.limit.selected = "soc" ct.data.chargemode.scheduled_charging.plans = {"0": plan} diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 947185ed29..9857b427f7 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -11,10 +11,10 @@ from typing import List, Optional, Tuple from control import data +from control.ev.charge_template import ChargeTemplate from control.chargepoint.chargepoint_state import ChargepointState, PHASE_SWITCH_STATES from control.chargepoint.charging_type import ChargingType from control.chargepoint.control_parameter import ControlParameter -from control.ev.charge_template import ChargeTemplate from control.ev.ev_template import EvTemplate from control.limiting_value import LimitingValue from dataclass_utils.factories import empty_list_factory @@ -86,10 +86,8 @@ class Ev: def __init__(self, index: int): try: self.ev_template: EvTemplate = EvTemplate() - self.charge_template: ChargeTemplate = ChargeTemplate(0) + self.charge_template: ChargeTemplate = ChargeTemplate() self.soc_module: ConfigurableVehicle = None - self.chargemode_changed = False - self.submode_changed = False self.num = index self.data = EvData() except Exception: @@ -114,6 +112,7 @@ def soc_interval_expired(self, vehicle_update_data: VehicleUpdateData) -> bool: return request_soc def get_required_current(self, + charge_template: ChargeTemplate, control_parameter: ControlParameter, max_phases_hw: int, phase_switch_supported: bool, @@ -143,8 +142,8 @@ def get_required_current(self, message = None state = True try: - if self.charge_template.data.chargemode.selected == "scheduled_charging": - plan_data = self.charge_template.scheduled_charging_recent_plan( + if charge_template.data.chargemode.selected == "scheduled_charging": + plan_data = charge_template.scheduled_charging_recent_plan( self.data.get.soc, self.ev_template, control_parameter.phases, @@ -159,13 +158,13 @@ def get_required_current(self, # 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. + charge_template.data.chargemode. 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: control_parameter.current_plan = None - required_current, submode, message, phases = self.charge_template.scheduled_charging_calc_current( + required_current, submode, message, phases = charge_template.scheduled_charging_calc_current( plan_data, self.data.get.soc, imported_since_plugged, @@ -177,8 +176,8 @@ 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): - tmp_current, tmp_submode, tmp_message, plan_id, phases = self.charge_template.time_charging( + charge_template.data.time_charging.active): + tmp_current, tmp_submode, tmp_message, plan_id, phases = charge_template.time_charging( self.data.get.soc, imported_since_plugged, charging_type @@ -190,22 +189,22 @@ def get_required_current(self, 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": - required_current, submode, tmp_message, phases = self.charge_template.instant_charging( + if charge_template.data.chargemode.selected == "instant_charging": + required_current, submode, tmp_message, phases = 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, tmp_message, phases = self.charge_template.pv_charging( + elif charge_template.data.chargemode.selected == "pv_charging": + required_current, submode, tmp_message, phases = 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, tmp_message, phases = self.charge_template.eco_charging( + elif charge_template.data.chargemode.selected == "eco_charging": + required_current, submode, tmp_message, phases = 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, tmp_message = self.charge_template.stop() + elif charge_template.data.chargemode.selected == "stop": + required_current, submode, tmp_message = 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"): + if submode == "stop" or (charge_template.data.chargemode.selected == "stop"): state = False if phases is None: log.debug("Keine Phasenvorgabe durch Lademodus. Behalte Phasenzahl bei.") @@ -216,18 +215,6 @@ def get_required_current(self, return (False, f"Kein Ladevorgang, da ein Fehler aufgetreten ist: {' '.join(e.args)}", "stop", 0, control_parameter.phases) - def set_chargemode_changed(self, control_parameter: ControlParameter, submode: str) -> None: - if ((submode == "time_charging" and control_parameter.chargemode != "time_charging") or - (submode != "time_charging" and - control_parameter.chargemode != self.charge_template.data.chargemode.selected)): - self.chargemode_changed = True - log.debug("Änderung des Lademodus") - else: - self.chargemode_changed = False - - def set_submode_changed(self, control_parameter: ControlParameter, submode: str) -> None: - self.submode_changed = (submode != control_parameter.submode) - def check_min_max_current(self, control_parameter: ControlParameter, required_current: float, @@ -275,6 +262,7 @@ def check_min_max_current(self, NOT_ENOUGH_POWER = ", da nicht ausreichend Überschuss für mehrphasiges Laden zur Verfügung steht." def _check_phase_switch_conditions(self, + charge_template: ChargeTemplate, control_parameter: ControlParameter, get_currents: List[float], get_power: float, @@ -288,7 +276,7 @@ def _check_phase_switch_conditions(self, phases_in_use = control_parameter.phases pv_config = data.data.general_data.data.chargemode_config.pv_charging max_phases_ev = self.ev_template.data.max_phases - if self.charge_template.data.chargemode.pv_charging.feed_in_limit: + if charge_template.data.chargemode.pv_charging.feed_in_limit: feed_in_yield = pv_config.feed_in_yield else: feed_in_yield = 0 @@ -311,6 +299,7 @@ def _check_phase_switch_conditions(self, PHASE_SWITCH_DELAY_TEXT = '{} Phasen in {}.' def auto_phase_switch(self, + charge_template: ChargeTemplate, control_parameter: ControlParameter, cp_num: int, get_currents: List[float], @@ -325,7 +314,7 @@ def auto_phase_switch(self, phases_in_use = control_parameter.phases pv_config = data.data.general_data.data.chargemode_config.pv_charging cm_config = data.data.general_data.data.chargemode_config - if self.charge_template.data.chargemode.pv_charging.feed_in_limit: + if charge_template.data.chargemode.pv_charging.feed_in_limit: feed_in_yield = pv_config.feed_in_yield else: feed_in_yield = 0 @@ -350,7 +339,8 @@ def auto_phase_switch(self, f'{required_reserved_power}W') # Wenn gerade umgeschaltet wird, darf kein Timer gestartet werden. if not self.ev_template.data.prevent_phase_switch: - condition, condition_msg = self._check_phase_switch_conditions(control_parameter, + condition, condition_msg = self._check_phase_switch_conditions(charge_template, + control_parameter, get_currents, get_power, max_current_cp, diff --git a/packages/control/ev/ev_template.py b/packages/control/ev/ev_template.py index 8da234a1ff..a8e8fbe02d 100644 --- a/packages/control/ev/ev_template.py +++ b/packages/control/ev/ev_template.py @@ -5,6 +5,7 @@ class EvTemplateData: dc_min_current: int = 0 dc_max_current: int = 0 + id: int = 0 name: str = "Fahrzeug-Profil" max_current_multi_phases: int = 16 max_phases: int = 3 @@ -33,4 +34,3 @@ class EvTemplate: data: EvTemplateData = field(default_factory=ev_template_data_factory, metadata={ "topic": "config"}) - et_num: int = 0 diff --git a/packages/helpermodules/command.py b/packages/helpermodules/command.py index c2aa3f9f70..0309ebf069 100644 --- a/packages/helpermodules/command.py +++ b/packages/helpermodules/command.py @@ -177,6 +177,17 @@ def setup_added_chargepoint(): Pub().pub(f'openWB/chargepoint/{new_id}/config', chargepoint_config) Pub().pub(f'openWB/chargepoint/{new_id}/set/manual_lock', False) {Pub().pub(f"openWB/chargepoint/{new_id}/get/"+k, v) for (k, v) in asdict(chargepoint.Get()).items()} + charge_template = SubData.ev_charge_template_data[f"ct{SubData.ev_data['ev0'].data.charge_template}"] + for time_plan in charge_template.data.time_charging.plans: + Pub().pub(f'openWB/chargepoint/{new_id}/set/charge_template/time_charging/plans', + dataclass_utils.asdict(time_plan)) + for scheduled_plan in charge_template.data.chargemode.scheduled_charging.plans: + Pub().pub(f'openWB/chargepoint/{new_id}/set/charge_template/chargemode/scheduled_charging/plans', + scheduled_plan) + charge_template = dataclass_utils.asdict(charge_template.data) + charge_template["chargemode"]["scheduled_charging"]["plans"].clear() + charge_template["time_charging"]["plans"].clear() + Pub().pub(f'openWB/chargepoint/{new_id}/set/charge_template', charge_template) self.max_id_hierarchy = self.max_id_hierarchy + 1 Pub().pub("openWB/set/command/max_id/hierarchy", self.max_id_hierarchy) if self.max_id_chargepoint_template == -1: @@ -338,6 +349,7 @@ def addChargeTemplate(self, connection_id: str, payload: dict) -> None: """ new_id = self.max_id_charge_template + 1 charge_template_default = get_new_charge_template() + charge_template_default["id"] = new_id Pub().pub("openWB/set/vehicle/template/charge_template/" + str(new_id), charge_template_default) self.max_id_charge_template = new_id diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index be3b1a1c7c..05bdb4bdee 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -73,11 +73,17 @@ def on_message(self, client: mqtt.Client, userdata, msg: mqtt.MQTTMessage): if "openWB/set/vehicle/" in msg.topic: if "openWB/set/vehicle/template/ev_template/" in msg.topic: self.event_ev_template.wait(5) + self.process_vehicle_ev_template_topic(msg) elif "openWB/set/vehicle/template/charge_template/" in msg.topic: self.event_charge_template.wait(5) - self.process_vehicle_topic(msg) + self.process_vehicle_charge_template_topic(msg) + else: + self.process_vehicle_topic(msg) elif "openWB/set/chargepoint/" in msg.topic: - self.process_chargepoint_topic(msg) + if "openWB/set/chargepoint/" in msg.topic and "/set/charge_template" in msg.topic: + self.process_vehicle_charge_template_topic(msg) + else: + self.process_chargepoint_topic(msg) elif "openWB/set/pv/" in msg.topic: self.process_pv_topic(msg) elif "openWB/set/bat/" in msg.topic: @@ -402,8 +408,6 @@ def process_vehicle_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, str) elif "/info" in msg.topic: self._validate_value(msg, "json") - elif "openWB/set/vehicle/template" in msg.topic: - self._subprocess_vehicle_chargemode_topic(msg) elif "openWB/set/vehicle/set/vehicle_update_completed" in msg.topic: self._validate_value(msg, bool) elif "/set/soc_error_counter" in msg.topic: @@ -437,7 +441,7 @@ def process_vehicle_topic(self, msg: mqtt.MQTTMessage): except Exception: log.exception(f"Fehler im setdata-Modul: Topic {msg.topic}, Value: {msg.payload}") - def _subprocess_vehicle_chargemode_topic(self, msg: mqtt.MQTTMessage): + def process_vehicle_charge_template_topic(self, msg: mqtt.MQTTMessage): """ Handler für die Lade-Profil-Topics Parameters ---------- @@ -514,6 +518,15 @@ def _subprocess_vehicle_chargemode_topic(self, msg: mqtt.MQTTMessage): except Exception: log.exception(f"Fehler im setdata-Modul: Topic {msg.topic}, Value: {msg.payload}") + def process_vehicle_ev_template_topic(self, msg: mqtt.MQTTMessage): + try: + if "ev_template" in msg.topic: + self._validate_value(msg, "json") + else: + self.__unknown_topic(msg) + except Exception: + log.exception(f"Fehler im setdata-Modul: Topic {msg.topic}, Value: {msg.payload}") + def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): """ Handler für die Ladepunkt-Topics @@ -538,7 +551,8 @@ def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, "json") elif subdata.SubData.cp_data.get(f"cp{get_index(msg.topic)}"): if ("/set/charging_ev" in msg.topic or - "/set/charging_ev_prev" in msg.topic): + "/set/charging_ev_prev" in msg.topic or + "/set/ev_prev" in msg.topic): self._validate_value(msg, int, [(-1, float("inf"))]) elif ("/set/current" in msg.topic or "/set/current_prev" in msg.topic): diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index e864c75bae..497cc2c141 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -300,11 +300,23 @@ def process_vehicle_topic(self, client: mqtt.Client, var: Dict[str, ev.Ev], msg: client.subscribe(f"openWB/vehicle/{index}/soc_module/general_config", 2) self.event_soc.set() else: + # temporäres ChargeTemplate aktualisieren, wenn dem Fahrzeug ein anderes Ladeprofil zugeordnet + # wird self.set_json_payload_class(var["ev"+index].data, msg) + if re.search("/vehicle/[0-9]+/charge_template$", msg.topic) is not None: + charge_template_id = int(decode_payload(msg.payload)) + if var["ev"+index].data.charge_template != charge_template_id: + ev_id = get_index(msg.topic) + for cp in self.cp_data.values(): + if ((cp.chargepoint.data.set.charging_ev != -1 and + cp.chargepoint.data.set.charging_ev == ev_id) or + cp.chargepoint.data.config.ev == ev_id): + cp.chargepoint.update_charge_template( + self.ev_charge_template_data[f"ct{charge_template_id}"]) except Exception: log.exception("Fehler im subdata-Modul") - def process_vehicle_charge_template_topic(self, var: Dict[str, ev.ChargeTemplate], msg: mqtt.MQTTMessage): + def process_vehicle_charge_template_topic(self, var: Dict[str, ChargeTemplate], msg: mqtt.MQTTMessage): """ Handler für die EV-Topics Parameter @@ -322,41 +334,56 @@ def process_vehicle_charge_template_topic(self, var: Dict[str, ev.ChargeTemplate var.pop("ct"+index) else: if "ct"+index not in var: - var["ct"+index] = ev.ChargeTemplate(int(index)) - if re.search("/vehicle/template/charge_template/[0-9]+/chargemode/scheduled_charging/plans/[0-9]+$", - msg.topic) is not None: - index_second = get_second_index(msg.topic) - if decode_payload(msg.payload) == "": - try: - var["ct"+index].data.chargemode.scheduled_charging.plans.pop(index_second) - except KeyError: - log.error("Es konnte kein Zielladen-Plan mit der ID " + - str(index_second)+" in dem Lade-Profil "+str(index)+" gefunden werden.") - else: - var["ct"+index].data.chargemode.scheduled_charging.plans[ - index_second] = dataclass_from_dict(ScheduledChargingPlan, decode_payload(msg.payload)) + var["ct"+index] = ChargeTemplate() + self.process_charge_template_topic(var["ct"+index], msg) + if re.search("/chargemode/scheduled_charging/plans/[0-9]+$", msg.topic) is not None: self.event_scheduled_charging_plan.set() - elif re.search("/vehicle/template/charge_template/[0-9]+/time_charging/plans/[0-9]+$", - msg.topic) is not None: - index_second = get_second_index(msg.topic) - if decode_payload(msg.payload) == "": - try: - var["ct"+index].data.time_charging.plans.pop(index_second) - except KeyError: - log.error("Es konnte kein Zeitladen-Plan mit der ID " + - str(index_second)+" in dem Lade-Profil "+str(index)+" gefunden werden.") - else: - var["ct"+index].data.time_charging.plans[ - index_second] = dataclass_from_dict(TimeChargingPlan, decode_payload(msg.payload)) + elif re.search("/time_charging/plans/[0-9]+$", msg.topic) is not None: self.event_time_charging_plan.set() else: - # Pläne unverändert übernehmen - scheduled_charging_plans = var["ct" + index].data.chargemode.scheduled_charging.plans - time_charging_plans = var["ct" + index].data.time_charging.plans - var["ct" + index].data = dataclass_from_dict(ChargeTemplateData, decode_payload(msg.payload)) - var["ct"+index].data.time_charging.plans = time_charging_plans - var["ct"+index].data.chargemode.scheduled_charging.plans = scheduled_charging_plans self.event_charge_template.set() + # Temporäres ChargeTemplate aktualisieren, wenn persistentes geändert wird + for vehicle in self.ev_data.values(): + if vehicle.data.charge_template == int(index): + for cp in self.cp_data.values(): + if ((cp.chargepoint.data.set.charging_ev != -1 and + cp.chargepoint.data.set.charging_ev == vehicle.num) or + cp.chargepoint.data.config.ev == vehicle.num): + cp.chargepoint.update_charge_template(var["ct"+index]) + except Exception: + log.exception("Fehler im subdata-Modul") + + def process_charge_template_topic(self, var: ChargeTemplate, msg: mqtt.MQTTMessage): + try: + if re.search("/chargemode/scheduled_charging/plans/[0-9]+$", msg.topic) is not None: + index_second = get_second_index(msg.topic) + if decode_payload(msg.payload) == "": + try: + var.data.chargemode.scheduled_charging.plans.pop(index_second) + except KeyError: + log.error(f"Es konnte kein Zielladen-Plan mit der ID {index_second} " + "in dem Lade-Profil gefunden werden.") + else: + var.data.chargemode.scheduled_charging.plans[ + index_second] = dataclass_from_dict(ScheduledChargingPlan, decode_payload(msg.payload)) + elif re.search("/time_charging/plans/[0-9]+$", msg.topic) is not None: + index_second = get_second_index(msg.topic) + if decode_payload(msg.payload) == "": + try: + var.data.time_charging.plans.pop(index_second) + except KeyError: + log.error("Es konnte kein Zeitladen-Plan mit der ID " + + str(index_second)+" in dem Lade-Profil gefunden werden.") + else: + var.data.time_charging.plans[ + index_second] = dataclass_from_dict(TimeChargingPlan, decode_payload(msg.payload)) + else: + # Pläne unverändert übernehmen + scheduled_charging_plans = var.data.chargemode.scheduled_charging.plans + time_charging_plans = var.data.time_charging.plans + var.data = dataclass_from_dict(ChargeTemplateData, decode_payload(msg.payload)) + var.data.time_charging.plans = time_charging_plans + var.data.chargemode.scheduled_charging.plans = scheduled_charging_plans except Exception: log.exception("Fehler im subdata-Modul") @@ -378,7 +405,7 @@ def process_vehicle_ev_template_topic(self, var: Dict[str, EvTemplate], msg: mqt var.pop("et"+index) else: if "et"+index not in var: - var["et"+index] = EvTemplate(et_num=int(index)) + var["et"+index] = EvTemplate() var["et" + index].data = dataclass_from_dict(EvTemplateData, decode_payload(msg.payload)) self.event_ev_template.set() except Exception: @@ -398,11 +425,12 @@ def process_chargepoint_topic(self, var: Dict[str, chargepoint.Chargepoint], msg if re.search("/chargepoint/[0-9]+/", msg.topic) is not None: index = get_index(msg.topic) if decode_payload(msg.payload) == "": - log.debug("Stop des Handlers für den internen Ladepunkt.") - self.event_stop_internal_chargepoint.set() - if "cp"+index in var: - var.pop("cp"+index) - self.set_internal_chargepoint_configured() + if re.search("/chargepoint/[0-9]+/config", msg.topic) is not None: + log.debug("Stop des Handlers für den internen Ladepunkt.") + self.event_stop_internal_chargepoint.set() + if "cp"+index in var: + var.pop("cp"+index) + self.set_internal_chargepoint_configured() else: if "cp"+index not in var: var["cp"+index] = ChargepointStateUpdate( @@ -418,7 +446,11 @@ def process_chargepoint_topic(self, var: Dict[str, chargepoint.Chargepoint], msg var["cp"+index].chargepoint.data.set.log = dataclass_from_dict( Log, decode_payload(msg.payload)) else: - self.set_json_payload_class(var["cp"+index].chargepoint.data.set, msg) + if "charge_template" in msg.topic: + self.process_charge_template_topic( + var["cp"+index].chargepoint.data.set.charge_template, msg) + else: + self.set_json_payload_class(var["cp"+index].chargepoint.data.set, msg) elif re.search("/chargepoint/[0-9]+/get/", msg.topic) is not None: if re.search("/chargepoint/[0-9]+/get/connected_vehicle/", msg.topic) is not None: self.set_json_payload_class(var["cp"+index].chargepoint.data.get.connected_vehicle, msg) diff --git a/packages/helpermodules/timecheck_test.py b/packages/helpermodules/timecheck_test.py index d5aaaf4a8f..c42e76c279 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, chargemode_switch_timestamp=1652680800) # angesteckt am 16.5.22 um 8:00 + plan, chargemode_switch_timestamp=datetime.datetime.strptime("5/16/2022 8:00", "%m/%d/%Y %H:%M").timestamp()) # evaluation assert remaining_time == expected_remaining_time diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 9a64bc9652..89ba54b305 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -9,7 +9,6 @@ import time from typing import List, Optional from paho.mqtt.client import Client as MqttClient, MQTTMessage - import dataclass_utils from control.chargepoint.chargepoint_template import get_chargepoint_template_default @@ -53,7 +52,7 @@ class UpdateConfig: - DATASTORE_VERSION = 76 + DATASTORE_VERSION = 78 valid_topic = [ "^openWB/bat/config/configured$", "^openWB/bat/config/power_limit_mode$", @@ -127,6 +126,9 @@ class UpdateConfig: "^openWB/chargepoint/[0-9]+/get/rfid$", "^openWB/chargepoint/[0-9]+/get/rfid_timestamp$", "^openWB/chargepoint/[0-9]+/set/charging_ev$", + "^openWB/chargepoint/[0-9]+/set/charge_template/time_charging/plans/[0-9]+$", + "^openWB/chargepoint/[0-9]+/set/charge_template/chargemode/scheduled_charging/plans/[0-9]+$", + "^openWB/chargepoint/[0-9]+/set/charge_template$", "^openWB/chargepoint/[0-9]+/set/current$", "^openWB/chargepoint/[0-9]+/set/energy_to_charge$", "^openWB/chargepoint/[0-9]+/set/manual_lock$", @@ -447,10 +449,10 @@ class UpdateConfig: ("openWB/counter/config/home_consumption_source_id", counter_all.Config().home_consumption_source_id), ("openWB/vehicle/0/name", "Standard-Fahrzeug"), ("openWB/vehicle/0/info", {"manufacturer": None, "model": None}), - ("openWB/vehicle/0/charge_template", ev.Ev(0).charge_template.ct_num), + ("openWB/vehicle/0/charge_template", ev.Ev(0).charge_template.data.id), ("openWB/vehicle/0/soc_module/config", NO_MODULE), ("openWB/vehicle/0/soc_module/general_config", dataclass_utils.asdict(GeneralVehicleConfig())), - ("openWB/vehicle/0/ev_template", ev.Ev(0).ev_template.et_num), + ("openWB/vehicle/0/ev_template", ev.Ev(0).ev_template.data.id), ("openWB/vehicle/0/tag_id", ev.Ev(0).data.tag_id), ("openWB/vehicle/0/get/soc", ev.Ev(0).data.get.soc), ("openWB/vehicle/template/ev_template/0", asdict(EvTemplateData(name="Standard-Fahrzeug-Profil", @@ -843,7 +845,7 @@ def upgrade(topic: str, payload) -> Optional[dict]: if re.search("openWB/vehicle/template/ev_template/[0-9]+$", topic) is not None: payload = decode_payload(payload) if "keep_charge_active_duration" not in payload: - payload["keep_charge_active_duration"] = ev.EvTemplateData().keep_charge_active_duration + payload["keep_charge_active_duration"] = EvTemplateData().keep_charge_active_duration return {topic: payload} self._loop_all_received_topics(upgrade) self.__update_topic("openWB/system/datastore_version", 8) @@ -2026,3 +2028,32 @@ def get_new_phases_to_use(topic) -> int: return topics 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: + if (re.search("openWB/vehicle/template/charge_template/[0-9]+", topic) is not None or + re.search("openWB/vehicle/template/ev_template/[0-9]+", topic) is not None): + payload = decode_payload(payload) + index = get_index(topic) + payload.update({"id": index}) + Pub().pub(topic, payload) + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 77) + + def upgrade_datastore_77(self) -> None: + def upgrade(topic: str, payload) -> None: + if re.search("openWB/chargepoint/[0-9]+/config", topic) is not None: + topics = {} + payload = decode_payload(payload) + index = get_index(topic) + charge_template_id = decode_payload( + self.all_received_topics[f'openWB/vehicle/{payload["ev"]}/charge_template']) + for template_topic, template_payload in self.all_received_topics.items(): + if f'openWB/vehicle/template/charge_template/{charge_template_id}' in template_topic: + topics.update( + {template_topic.replace(f'openWB/vehicle/template/charge_template/{charge_template_id}', + f"openWB/chargepoint/{index}/set/charge_template"): + decode_payload(template_payload)}) + return topics + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 78) diff --git a/packages/modules/web_themes/standard_legacy/web/index.html b/packages/modules/web_themes/standard_legacy/web/index.html index 60d01051d3..b4a687d93f 100644 --- a/packages/modules/web_themes/standard_legacy/web/index.html +++ b/packages/modules/web_themes/standard_legacy/web/index.html @@ -500,7 +500,7 @@