diff --git a/.github/workflows/build_web_theme_koala.yml b/.github/workflows/build_web_theme_koala.yml new file mode 100644 index 0000000000..f7c49cb5f6 --- /dev/null +++ b/.github/workflows/build_web_theme_koala.yml @@ -0,0 +1,37 @@ +name: Build Web Theme Koala + +on: + push: + paths: + - packages/modules/web_themes/koala/source/** + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js (v20) + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: packages/modules/web_themes/koala/source/package-lock.json + + - name: Install Dependencies and Build + run: | + cd packages/modules/web_themes/koala/source + npm install + npm run build --if-present + + - name: Commit and Push Changes + run: | + git config user.name "${{ github.actor }}" + git config user.email "${{ github.actor }}@users.noreply.github.com" + git add packages/modules/web_themes/koala/web + git commit -m "Build Web Theme: Koala" + git push diff --git a/data/config/mosquitto/openwb_local.conf b/data/config/mosquitto/openwb_local.conf index 1518d66272..f540b3dcf1 100644 --- a/data/config/mosquitto/openwb_local.conf +++ b/data/config/mosquitto/openwb_local.conf @@ -1,4 +1,4 @@ -# openwb-version:18 +# openwb-version:19 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..99ce834534 100644 --- a/docs/Ladeprofile.md +++ b/docs/Ladeprofile.md @@ -3,3 +3,13 @@ _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.8) + +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 in den Einstellungen geändert wird. diff --git a/docs/MQTT.md b/docs/MQTT.md index 43a7ca32a2..a905d54195 100644 --- a/docs/MQTT.md +++ b/docs/MQTT.md @@ -9,7 +9,7 @@ openWB hat einen eigenen MQTT-Broker integriert, über den die Kommunikation lä Als EVU-Zähler können auch Werte über MQTT empfangen werden. Die Integration ist im Abschnitt [Zähler](https://github.com/openWB/core/wiki/Zähler) beschrieben. -## Smarthome +## SmartHome ## Steuerbefehle @@ -32,8 +32,8 @@ SoC-Limit auf z.B. 80% setzen Zielladen `openWB/set/vehicle/template/charge_template/#/chargemode/selected -> scheduled_charging` -Standby -`openWB/set/vehicle/template/charge_template/#/chargemode/selected -> standby` +Eco +`openWB/set/vehicle/template/charge_template/#/chargemode/selected -> eco_charging` Stop `openWB/set/vehicle/template/charge_template/#/chargemode/selected -> stop` @@ -50,7 +50,7 @@ openWB/vehicle/template/charge_template/1 Setzen von min_Current für min+PV nachbauen: `openWB/set/vehicle/template/charge_template/#/chargemode/pv_charging/min_current` -Setzen des Lademodus: (Werte die zu senden sind: instant_charging, pv_charging, scheduled_charging, standby, stop) +Setzen des Lademodus: (Werte die zu senden sind: instant_charging, pv_charging, scheduled_charging, eco_charging, stop) `openWB/set/vehicle/template/charge_template/#/chargemode/selected` Ladepunkt sperren für Priosteuerung der LP: diff --git a/packages/control/algorithm/additional_current_test.py b/packages/control/algorithm/additional_current_test.py index 35ac25410a..e1f57b6309 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/chargemodes.py b/packages/control/algorithm/chargemodes.py index 65e16a9645..76bc9906a6 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: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[12: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..d1241ec512 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.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() @@ -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/algorithm/surplus_controlled.py b/packages/control/algorithm/surplus_controlled.py index 3c339fe02a..ca649d23ab 100644 --- a/packages/control/algorithm/surplus_controlled.py +++ b/packages/control/algorithm/surplus_controlled.py @@ -99,9 +99,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 @@ -113,7 +113,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,55 +153,65 @@ def check_submode_pv_charging(self) -> None: evu_counter = data.data.counter_all_data.get_evu_counter() for cp in get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_PV_ONLY): - 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 control_parameter.state == ChargepointState.CHARGING_ALLOWED: - if cp.data.set.charging_ev_data.ev_template.data.prevent_charge_stop is False: - threshold = evu_counter.calc_switch_off_threshold(cp)[0] - if evu_counter.calc_raw_surplus() - cp.data.set.required_power < threshold: - control_parameter.required_currents = [0]*3 - control_parameter.state = ChargepointState.NO_CHARGING_ALLOWED + try: + 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.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: + threshold = evu_counter.calc_switch_off_threshold(cp)[0] + if evu_counter.calc_raw_surplus() - cp.data.set.required_power < threshold: + control_parameter.required_currents = [0]*3 + control_parameter.state = ChargepointState.NO_CHARGING_ALLOWED + else: + control_parameter.required_currents = [0]*3 else: - control_parameter.required_currents = [0]*3 - else: - if ((control_parameter.state == ChargepointState.CHARGING_ALLOWED or - control_parameter.state == ChargepointState.SWITCH_OFF_DELAY) and - phase_switch_necessary() is False): - evu_counter.switch_off_check_threshold(cp) - if control_parameter.state == ChargepointState.SWITCH_OFF_DELAY: - evu_counter.switch_off_check_timer(cp) - if control_parameter.state == ChargepointState.SWITCH_ON_DELAY: - # Wenn charge_state False und set_current > 0, will Auto nicht laden - evu_counter.switch_on_timer_expired(cp) - if control_parameter.state not in CHARGING_STATES: - control_parameter.required_currents = [0]*3 + if ((control_parameter.state == ChargepointState.CHARGING_ALLOWED or + control_parameter.state == ChargepointState.SWITCH_OFF_DELAY) and + phase_switch_necessary() is False): + evu_counter.switch_off_check_threshold(cp) + if control_parameter.state == ChargepointState.SWITCH_OFF_DELAY: + evu_counter.switch_off_check_timer(cp) + if control_parameter.state == ChargepointState.SWITCH_ON_DELAY: + # Wenn charge_state False und set_current > 0, will Auto nicht laden + evu_counter.switch_on_timer_expired(cp) + if control_parameter.state not in CHARGING_STATES: + control_parameter.required_currents = [0]*3 + except Exception: + log.exception(f"Fehler in der PV-gesteuerten Ladung bei {cp.num}") def check_switch_on(self) -> None: for cp in get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_PV_ONLY): - if (cp.data.control_parameter.state == ChargepointState.NO_CHARGING_ALLOWED or - cp.data.control_parameter.state == ChargepointState.SWITCH_ON_DELAY): - data.data.counter_all_data.get_evu_counter().switch_on_threshold_reached(cp) + try: + if (cp.data.control_parameter.state == ChargepointState.NO_CHARGING_ALLOWED or + cp.data.control_parameter.state == ChargepointState.SWITCH_ON_DELAY): + data.data.counter_all_data.get_evu_counter().switch_on_threshold_reached(cp) + except Exception: + log.exception(f"Fehler in der PV-gesteuerten Ladung bei {cp.num}") def set_required_current_to_max(self) -> None: for cp in get_chargepoints_by_chargemodes(CONSIDERED_CHARGE_MODES_SURPLUS): - charging_ev_data = cp.data.set.charging_ev_data - required_currents = cp.data.control_parameter.required_currents - control_parameter = cp.data.control_parameter - - if control_parameter.phases == 1: - max_current = charging_ev_data.ev_template.data.max_current_single_phase - else: - max_current = charging_ev_data.ev_template.data.max_current_multi_phases + try: + charging_ev_data = cp.data.set.charging_ev_data + required_currents = cp.data.control_parameter.required_currents + control_parameter = cp.data.control_parameter - if cp.template.data.charging_type == ChargingType.AC.value: if control_parameter.phases == 1: max_current = charging_ev_data.ev_template.data.max_current_single_phase else: max_current = charging_ev_data.ev_template.data.max_current_multi_phases - else: - max_current = charging_ev_data.ev_template.data.dc_max_current - control_parameter.required_currents = [max_current if required_currents[i] != 0 else 0 for i in range(3)] - control_parameter.required_current = max_current + if cp.template.data.charging_type == ChargingType.AC.value: + if control_parameter.phases == 1: + max_current = charging_ev_data.ev_template.data.max_current_single_phase + else: + max_current = charging_ev_data.ev_template.data.max_current_multi_phases + else: + max_current = charging_ev_data.ev_template.data.dc_max_current + + control_parameter.required_currents = [ + max_current if required_currents[i] != 0 else 0 for i in range(3)] + control_parameter.required_current = max_current + except Exception: + log.exception(f"Fehler in der PV-gesteuerten Ladung bei {cp.num}") 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 a7ae74224e..8e3ff229a0 100644 --- a/packages/control/auto_phase_switch_test.py +++ b/packages/control/auto_phase_switch_test.py @@ -6,6 +6,7 @@ from control.counter import Counter, CounterData, Set from control.limiting_value import LoadmanagementLimit +from control.ev.charge_template import ChargeTemplate from control.pv_all import PvAll from control.bat_all import BatAll from control.general import General @@ -132,7 +133,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/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/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index ae2ecfe00e..e3945fff03 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 @@ -30,9 +31,12 @@ 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.abstract_plans import ScheduledChargingPlan, TimeChargingPlan +from helpermodules.broker import BrokerClient from helpermodules.phase_mapping import convert_single_evu_phase_to_cp_phase from helpermodules.pub import Pub from helpermodules import timecheck @@ -71,6 +75,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) @@ -209,7 +215,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 @@ -218,6 +224,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(data.data.ev_data["ev"+str(self.data.set.charging_ev_prev)].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", @@ -263,8 +271,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 @@ -286,9 +294,11 @@ def remember_previous_values(self): self.data.set.charge_state_prev = self.data.get.charge_state 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/charge_state_prev", self.data.set.charge_state_prev) 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() @@ -307,8 +317,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): @@ -490,18 +498,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 - (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. @@ -509,7 +508,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. @@ -528,10 +527,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: @@ -611,6 +610,19 @@ 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") + self.data.control_parameter.timestamp_chargemode_changed = create_timestamp() + 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 +635,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: @@ -640,14 +654,15 @@ 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.set.charge_template, self.data.control_parameter, - 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.control_parameter.timestamp_chargemode_changed, + 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) required_current = self.chargepoint_module.add_conversion_loss_to_current(required_current) @@ -655,20 +670,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 +699,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 +709,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", mittlerer Ist-Strom: {get_medium_charging_current(self.data.get.currents)}") except Exception: log.exception("Fehler im Prepare-Modul für Ladepunkt "+str(self.num)) @@ -706,8 +721,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) try: # check für charging stop or charging interruption, if so force a soc query for the ev if self.data.set.charge_state_prev and self.data.get.charge_state is False: @@ -727,6 +747,8 @@ def update(self, ev_list: Dict[str, Ev]) -> None: self.data.get.imported) Pub().pub("openWB/set/chargepoint/"+str(self.num) + "/set/ocpp_transaction_id", self.data.set.ocpp_transaction_id) + if self.data.get.plug_state and self.data.set.plug_state_prev is False: + self.data.control_parameter.timestamp_chargemode_changed = create_timestamp() # SoC nach Anstecken aktualisieren if ((self.data.get.plug_state and self.data.set.plug_state_prev is False) or (self.data.get.plug_state is False and self.data.set.plug_state_prev) or @@ -755,9 +777,13 @@ def _get_charging_ev(self, vehicle: int, ev_list: Dict[str, Ev]) -> Ev: " verwendet.") charging_ev = ev_list["ev0"] vehicle = 0 - if self.data.set.charging_ev != vehicle and self.data.set.charging_ev_prev != vehicle: + if (self.data.set.charging_ev != -1 and self.data.set.charging_ev != vehicle and + self.data.set.charging_ev_prev != 1 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) @@ -765,6 +791,54 @@ 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 _clear_template_topics(self, topic: str) -> None: + def on_connect(client, userdata, flags, rc): + client.subscribe(topic, 2) + + def __get_payload(client, userdata, msg): + received_topics.append(msg.topic) + received_topics = [] + BrokerClient("processBrokerBranch", on_connect, __get_payload).start_finite_loop() + for topic in received_topics: + Pub().pub(topic, "") + + def update_charge_template(self, charge_template: ChargeTemplate) -> None: + self._clear_template_topics(f'openWB/chargepoint/{self.num}/set/charge_template/#') + 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 update_bare_charge_template(self, charge_template: ChargeTemplate) -> None: + self._clear_template_topics(f"openWB/chargepoint/{self.num}/set/charge_template") + 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) + + def update_charge_template_scheduled_plan(self, plan: ScheduledChargingPlan) -> None: + self._clear_template_topics( + f"openWB/chargepoint/{self.num}/set/charge_template/chargemode/scheduled_charging/plans/{plan.id}") + Pub().pub(f"openWB/set/chargepoint/{self.num}" + f"/set/charge_template/chargemode/scheduled_charging/plans/{plan.id}", + dataclasses.asdict(plan)) + + def update_charge_template_time_plan(self, plan: TimeChargingPlan) -> None: + self._clear_template_topics( + f"openWB/chargepoint/{self.num}/set/charge_template/time_charging/plans/{plan.id}") + Pub().pub(f"openWB/set/chargepoint/{self.num}/set/charge_template/time_charging/plans/{plan.id}", + dataclasses.asdict(plan)) + def _pub_connected_vehicle(self, vehicle: Ev): """ published die Daten, die zur Anzeige auf der Hauptseite benötigt werden. @@ -788,43 +862,39 @@ 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 - data.data.general_data.get_phases_chargemode( - Chargemode.PV_CHARGING.value, - control_parameter.submode) == 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 - data.data.general_data.get_phases_chargemode(Chargemode.SCHEDULED_CHARGING.value, - control_parameter.submode) == 0) + self.data.set.charge_template.data.chargemode.scheduled_charging.plans[ + str(self.data.control_parameter.current_plan)].phases_to_use_pv == 0) if (self.cp_ev_support_phase_switch() and self.data.get.charge_state and (pv_auto_switch or scheduled_auto_switch) and diff --git a/packages/control/chargepoint/chargepoint_data.py b/packages/control/chargepoint/chargepoint_data.py index 722058af8c..44eda96904 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 @@ -121,6 +122,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) @@ -133,8 +138,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/chargepoint/control_parameter.py b/packages/control/chargepoint/control_parameter.py index 4b153067d0..0d221308db 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[LoadmanagementLimit] = field(default_factory=loadmanagement_limit_factory, metadata={ "topic": "control_parameter/limit"}) min_current: int = field(default=6, metadata={"topic": "control_parameter/min_current"}) @@ -29,6 +25,8 @@ class ControlParameter: submode: Chargemode_enum = field(default=Chargemode_enum.STOP, metadata={"topic": "control_parameter/submode"}) timestamp_charge_start: Optional[float] = field( default=None, metadata={"topic": "control_parameter/timestamp_charge_start"}) + timestamp_chargemode_changed: Optional[float] = field( + default=None, metadata={"topic": "control_parameter/timestamp_chargemode_changed"}) timestamp_last_phase_switch: float = field( default=0, metadata={"topic": "control_parameter/timestamp_last_phase_switch"}) timestamp_switch_on_off: Optional[float] = field( 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 8bd8dc845e..5ede5b128e 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -8,7 +8,6 @@ from control import data from control.algorithm.utils import get_medium_charging_current -from control.chargemode import Chargemode from control.ev.ev import Ev from control.chargepoint.chargepoint import Chargepoint from control.chargepoint.chargepoint_state import ChargepointState @@ -266,7 +265,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 @@ -276,7 +275,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 @@ -326,6 +325,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): @@ -338,14 +338,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 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 = 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 + 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 @@ -386,7 +386,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 a927f66e3a..04eb0cc8d1 100644 --- a/packages/control/ev/charge_template.py +++ b/packages/control/ev/charge_template.py @@ -2,13 +2,14 @@ import datetime 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,33 +30,49 @@ 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 +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 + current: int = 16 dc_current: float = 145 limit: Limit = field(default_factory=limit_factory) + phases_to_use: int = 3 @dataclass 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 eco_charging_factory() -> EcoCharging: + return EcoCharging() def pv_charging_factory() -> PvCharging: @@ -72,7 +89,8 @@ def instant_charging_factory() -> InstantCharging: @dataclass class Chargemode: - selected: str = "stop" + selected: str = "instant_charging" + 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) @@ -86,22 +104,12 @@ 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: + id: int = 0 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) @@ -113,26 +121,25 @@ 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 - id: int = 0 + plan: Optional[ScheduledChargingPlan] = None @dataclass class ChargeTemplate: """ Klasse der Lade-Profile """ - ct_num: int data: ChargeTemplateData = field(default_factory=charge_template_data_factory, metadata={ "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 = "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_CONFIGURED = "Zeitladen aktiviert, aber keine Zeitfenster konfiguriert." TIME_CHARGING_NO_PLAN_ACTIVE = "Keine Ladung, da kein Zeitfenster für Zeitladen aktiv ist." TIME_CHARGING_SOC_REACHED = "Kein Zeitladen, da der Soc bereits erreicht wurde." TIME_CHARGING_AMOUNT_REACHED = "Kein Zeitladen, da die Energiemenge bereits geladen wurde." @@ -140,216 +147,256 @@ 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 + phases = 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: - 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 - log.debug(message) - return 0, "stop", message, None + current = 0 + sub_mode = "stop" + 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 + 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) - 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], - imported_instant_charging: float, - charging_type: str) -> Tuple[int, str, Optional[str]]: + used_amount: float, + 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: - 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 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 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: - return current, "instant_charging", message - else: - return 0, "stop", self.INSTANT_CHARGING_AMOUNT_REACHED - else: - raise TypeError(f'{instant_charging.limit.selected} unbekanntes Sofortladen-Limit.') + if used_amount >= self.data.chargemode.instant_charging.limit.amount: + current = 0 + sub_mode = "stop" + 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() + 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_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, + 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 + 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 used_amount >= pv_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() + return 0, "stop", "Keine Ladung, da ein interner Fehler aufgetreten ist: "+traceback.format_exc(), 1 + + def eco_charging(self, + soc: Optional[float], + min_current: 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 + 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 + 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_charging_allowed(eco_charging.max_price): + 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 + except Exception: + 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, soc: float, ev_template: EvTemplate, phases: int, used_amount: float, - max_phases: 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 - 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 - charging_type == ChargingType.AC.value and - instant_phases == 0 and - plan_data.remaining_time > 300 and - self.data.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 - 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 - plan_data = self._search_plan(max_current, soc, ev_template, phases, used_amount, charging_type) - return plan_data - - def _search_plan(self, - max_current: int, - soc: Optional[float], - ev_template: EvTemplate, - 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 - 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, + chargemode_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: + 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: - 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 - plan_data = SelectedPlan( - remaining_time=remaining_time, - available_current=available_current, - max_current=max_current, - phases=phases, - id=plan.id, - 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.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)) - return plan_data + if plans_diff_end_date: + # ermittle den Key vom kleinsten value in plans_diff_end_date + 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 + + 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, + control_parameter_phases) -> SelectedPlan: + if plan.phases_to_use == 0: + 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) + 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 + + 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, plan: ScheduledChargingPlan, @@ -359,6 +406,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 @@ -384,8 +432,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 ' @@ -395,23 +442,31 @@ 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, 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 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 + 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 elif limit.selected == "soc" and limit.soc_scheduled <= soc < limit.soc_limit: @@ -420,48 +475,48 @@ 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 + 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 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 = 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), 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: - hour_list = data.data.optional_data.et_get_loading_hours(plan_data.duration, plan_data.remaining_time) + if plan.et_active: + 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 + current = plan_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: @@ -470,7 +525,7 @@ def scheduled_charging_calc_current(self, message = self.SCHEDULED_REACHED_LIMIT_SOC else: now = datetime.datetime.today() - start_time = now + datetime.timedelta(seconds=plan_data.remaining_time) + start_time = now + datetime.timedelta(seconds=selected_plan.remaining_time) if start_time.year == now.year and start_time.month == now.month and start_time.day == now.day: message = self.SCHEDULED_CHARGING_USE_PV.format( f"um {start_time.strftime('%-H:%M')} Uhr") @@ -479,11 +534,8 @@ 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]: - 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/charge_template_test.py b/packages/control/ev/charge_template_test.py index f113757322..08f88c8fc9 100644 --- a/packages/control/ev/charge_template_test.py +++ b/packages/control/ev/charge_template_test.py @@ -1,14 +1,15 @@ 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 +from control.ev.charge_template import ChargeTemplate from control.ev.ev_template import EvTemplate, EvTemplateData from control.general import General from helpermodules import timecheck @@ -25,32 +26,33 @@ 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, @@ -58,7 +60,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) @@ -73,20 +75,20 @@ 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.INSTANT_CHARGING_SOC_REACHED), + pytest.param("none", 0, 0, (16, "instant_charging", None, 3), id="without limit"), + pytest.param("soc", None, 0, (16, "instant_charging", None, 3), id="limit soc: soc not defined"), + pytest.param("soc", 49, 0, (16, "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.INSTANT_CHARGING_AMOUNT_REACHED), + pytest.param("amount", 0, 999, (16, "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 = ChargeTemplate() ct.data.chargemode.instant_charging.limit.selected = selected # execution @@ -97,72 +99,65 @@ 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.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, "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), id="soc not defined"), - pytest.param(15, 8, 15, (8, "instant_charging", ChargeTemplate.PV_CHARGING_MIN_CURRENT_CHARGING), + 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), 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, - expected: Tuple[int, str, Optional[str]]): +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 + ct.data.chargemode.pv_charging.phases_to_use_min_soc = 3 + 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 -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, (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), (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, + 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(side_effect=calc_duration) + monkeypatch.setattr(ChargeTemplate, "_calculate_duration", calculate_duration_mock) + evt = Mock(spec=EvTemplate, data=Mock(spec=EvTemplateData, battery_capacity=85)) # 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, 6000, 50, evt, 3000, max_hw_phases, phase_switch_supported, ChargingType.AC.value, 2) + # 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( @@ -173,7 +168,7 @@ def test_scheduled_charging_recent_plan(params: Params, monkeypatch): ]) 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 @@ -185,36 +180,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([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_search_plan(check_duration_return1: Tuple[Optional[float], bool], - check_duration_return2: Tuple[Optional[float], bool], - expected_plan_num: Optional[int], - monkeypatch): +def test_scheduled_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_duration", check_duration_mock) - ct = ChargeTemplate(0) + 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) + 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")) - 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.id == expected_plan_num - assert plan_data.duration == 100 + selected_plan = None @pytest.mark.parametrize( @@ -225,7 +220,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 +240,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, @@ -254,14 +249,16 @@ 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 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) + ret = ct.scheduled_charging_calc_current(plan_data, soc, used_amount, 3, 6, 0, ChargingType.AC.value, EvTemplate()) # evaluation assert ret == expected @@ -269,10 +266,10 @@ 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) + 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) @@ -284,15 +281,15 @@ 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 - ct = ChargeTemplate(0) + ct = ChargeTemplate() 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()]) @@ -301,7 +298,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, ChargingType.AC.value, EvTemplate()) # evaluation assert ret == expected diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index 077d7d8127..11bf4a82d4 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -11,11 +11,11 @@ from typing import List, Optional, Tuple from control import data +from control.ev.charge_template import ChargeTemplate from control.algorithm.utils import get_medium_charging_current 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, LoadmanagementLimit from dataclass_utils.factories import empty_list_factory @@ -87,10 +87,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: @@ -115,11 +113,13 @@ def soc_interval_expired(self, vehicle_update_data: VehicleUpdateData) -> bool: return request_soc def get_required_current(self, + charge_template: ChargeTemplate, control_parameter: ControlParameter, - imported: float, max_phases_hw: int, phase_switch_supported: bool, - charging_type: str) -> Tuple[bool, Optional[str], str, float, int]: + charging_type: str, + 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 @@ -141,108 +141,85 @@ def get_required_current(self, required_current = None submode = None message = None + tmp_message = None 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( + 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, - used_amount, + imported_since_plugged, max_phases_hw, phase_switch_supported, - charging_type) + charging_type, + 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.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"): + 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.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( + required_current, submode, tmp_message, phases = 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) + soc_request_interval_offset, + charging_type, + self.ev_template) + message = f"{tmp_message or ''}".strip() # 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 = 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, - 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 = 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, - used_amount, + imported_since_plugged, charging_type) - elif self.charge_template.data.chargemode.selected == "pv_charging": - required_current, submode, message = 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"): + 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 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 charge_template.data.chargemode.selected == "stop": + required_current, submode, tmp_message = charge_template.stop() + phases = control_parameter.phases or max_phases_hw + else: + tmp_message = None + message = f"{message or ''} {tmp_message or ''}".strip() + if submode == "stop" or (charge_template.data.chargemode.selected == "stop"): state = False - if phases is None: - phases = control_parameter.phases + 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)) 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, @@ -290,6 +267,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, @@ -303,7 +281,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 @@ -328,6 +306,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], @@ -342,7 +321,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 @@ -367,7 +346,8 @@ def auto_phase_switch(self, f'neue Leistung: {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/control/general.py b/packages/control/general.py index 8207891937..2904b9eaa0 100644 --- a/packages/control/general.py +++ b/packages/control/general.py @@ -7,22 +7,11 @@ from control import data from control.bat_all import BatConsiderationMode -from control.chargemode import Chargemode from helpermodules import timecheck 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] @@ -39,8 +28,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={ @@ -63,39 +50,14 @@ 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={ - "topic": "chargemode_config/time_charging/phases_to_use"}) - - -def time_charging_factory() -> TimeCharging: - return 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) 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"}) unbalanced_load: bool = field(default=False, metadata={ @@ -149,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/control/optional.py b/packages/control/optional.py index 5d936d8e9c..0b87d1fe38 100644 --- a/packages/control/optional.py +++ b/packages/control/optional.py @@ -38,7 +38,7 @@ def monitoring_stop(self): if self.mon_module is not None: self.mon_module.stop_monitoring() - def _et_provider_available(self) -> bool: + def et_provider_available(self) -> bool: return self.et_module is not None def et_charging_allowed(self, max_price: float): @@ -50,7 +50,7 @@ def et_charging_allowed(self, max_price: float): False: Preis liegt darüber """ try: - if self._et_provider_available(): + if self.et_provider_available(): if self.et_get_current_price() <= max_price: return True else: @@ -65,7 +65,7 @@ def et_charging_allowed(self, max_price: float): return False def et_get_current_price(self): - if self._et_provider_available(): + if self.et_provider_available(): return self.data.et.get.prices[str(int(create_unix_timestamp_current_full_hour()))] else: raise Exception("Kein Anbieter für strompreisbasiertes Laden konfiguriert.") @@ -81,7 +81,7 @@ def et_get_loading_hours(self, duration: float, remaining_time: float) -> List[i ------ list: Key des Dictionary (Unix-Sekunden der günstigen Stunden) """ - if self._et_provider_available() is False: + if self.et_provider_available() is False: raise Exception("Kein Anbieter für strompreisbasiertes Laden konfiguriert.") try: prices = self.data.et.get.prices diff --git a/packages/control/optional_test.py b/packages/control/optional_test.py index 8e3c4d5042..cc517ef7ae 100644 --- a/packages/control/optional_test.py +++ b/packages/control/optional_test.py @@ -7,7 +7,7 @@ def test_et_get_loading_hours(monkeypatch): opt = Optional() opt.data.et.get.prices = PRICE_LIST mock_et_provider_available = Mock(return_value=True) - monkeypatch.setattr(opt, "_et_provider_available", mock_et_provider_available) + monkeypatch.setattr(opt, "et_provider_available", mock_et_provider_available) # execution loading_hours = opt.et_get_loading_hours(3600, 7200) diff --git a/packages/helpermodules/abstract_plans.py b/packages/helpermodules/abstract_plans.py index 60586a9944..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: @@ -63,19 +64,23 @@ 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 @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/command.py b/packages/helpermodules/command.py index 511464e8ec..8cbce88ada 100644 --- a/packages/helpermodules/command.py +++ b/packages/helpermodules/command.py @@ -237,6 +237,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: @@ -398,6 +409,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/command_test.py b/packages/helpermodules/command_test.py index 6664cc1fca..dade5c3324 100644 --- a/packages/helpermodules/command_test.py +++ b/packages/helpermodules/command_test.py @@ -16,7 +16,7 @@ @pytest.fixture def subdata_fixture() -> None: - SubData(*([Mock()]*19)) + SubData(*([Mock()]*16)) SubData.cp_data = {"cp0": Mock(spec=ChargepointStateUpdate, chargepoint=Mock( spec=Chargepoint, chargepoint_module=Mock(spec=ChargepointModulePro)))} 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: diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index adaaa6062b..77d188332e 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -13,8 +13,7 @@ from helpermodules import hardware_configuration, subdata from helpermodules.broker import BrokerClient from helpermodules.pub import Pub, pub_single -from helpermodules.utils.topic_parser import (decode_payload, get_index, get_index_position, get_second_index, - get_second_index_position) +from helpermodules.utils.topic_parser import decode_payload, get_index, get_index_position from helpermodules.update_config import UpdateConfig import dataclass_utils @@ -25,17 +24,11 @@ class SetData: def __init__(self, event_ev_template: threading.Event, - event_charge_template: threading.Event, event_cp_config: threading.Event, - event_scheduled_charging_plan: threading.Event, - event_time_charging_plan: threading.Event, event_soc: threading.Event, event_subdata_initialized: threading.Event): self.event_ev_template = event_ev_template - self.event_charge_template = event_charge_template self.event_cp_config = event_cp_config - self.event_scheduled_charging_plan = event_scheduled_charging_plan - self.event_time_charging_plan = event_time_charging_plan self.event_soc = event_soc self.event_subdata_initialized = event_subdata_initialized self.heartbeat = False @@ -73,11 +66,16 @@ 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: @@ -159,42 +157,7 @@ def _validate_value(self, msg: mqtt.MQTTMessage, data_type, ranges=[], collectio else: # aktuelles json-Objekt liegt in subdata index = get_index(msg.topic) - if "time_charging" in msg.topic and "plans" in msg.topic: - index_second = get_second_index(msg.topic) - event = self.event_time_charging_plan - try: - template = dataclasses.asdict(copy.deepcopy( - subdata.SubData.ev_charge_template_data[ - "ct"+index].data.time_charging.plans[index_second])) - except IndexError: - template = {} - elif "scheduled_charging" in msg.topic and "plans" in msg.topic: - index_second = get_second_index(msg.topic) - event = self.event_scheduled_charging_plan - try: - template = dataclasses.asdict(copy.deepcopy( - subdata.SubData.ev_charge_template_data[ - "ct"+index].data.chargemode.scheduled_charging.plans[index_second])) - except IndexError: - template = {} - elif "charge_template" in msg.topic: - event = self.event_charge_template - if "ct"+str(index) in subdata.SubData.ev_charge_template_data: - template = dataclass_utils.asdict(copy.deepcopy( - subdata.SubData.ev_charge_template_data["ct"+str(index)].data)) - # Wenn eine Einzeleinstellung empfangen wird, muss das gesamte Profil veröffentlicht - # werden (pub_json=True), allerdings ohne Pläne. Diese sind in einem Extra-Topic. - try: - template["chargemode"]["scheduled_charging"].pop("plans") - except KeyError: - log.debug("Key 'plans' nicht gefunden, keine Zielladen-Pläne vorhanden.") - try: - template["time_charging"].pop("plans") - except KeyError: - log.debug("Key 'plans' nicht gefunden, keine Zeitladen-Pläne vorhanden.") - else: - template = {} - elif "ev_template" in msg.topic: + if "ev_template" in msg.topic: event = self.event_ev_template if "et"+str(index) in subdata.SubData.ev_template_data: template = copy.deepcopy( @@ -220,9 +183,6 @@ def _validate_value(self, msg: mqtt.MQTTMessage, data_type, ranges=[], collectio # Wert, der aktualisiert werden soll, erstellen/finden und updaten if event == self.event_cp_config: key_list = msg.topic.split("/")[5:] - elif (event == self.event_scheduled_charging_plan or - event == self.event_time_charging_plan): - key_list = msg.topic.split("/")[-1:] else: key_list = msg.topic.split("/")[6:] self._change_key(template, key_list, value) @@ -230,8 +190,6 @@ def _validate_value(self, msg: mqtt.MQTTMessage, data_type, ranges=[], collectio index_pos = get_index_position(msg.topic) if event == self.event_cp_config: topic = msg.topic[:index_pos]+"/config" - elif event == self.event_scheduled_charging_plan or event == self.event_time_charging_plan: - topic = msg.topic[:get_second_index_position(msg.topic)] elif event == self.event_soc: topic = msg.topic[:index_pos]+"/soc_module/calculated_soc_state" else: @@ -406,8 +364,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: @@ -441,7 +397,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 ---------- @@ -450,57 +406,15 @@ def _subprocess_vehicle_chargemode_topic(self, msg: mqtt.MQTTMessage): """ try: if "charge_template" in msg.topic: - if "/name" in msg.topic: - self._validate_value(msg, str, pub_json=True) - elif ("/load_default" in msg.topic or - "/prio" in msg.topic): - self._validate_value(msg, bool, pub_json=True) - elif "/chargemode/selected" in msg.topic: - self._validate_value(msg, str, pub_json=True) - elif "/chargemode/instant_charging/current" in msg.topic: - self._validate_value(msg, int, [(6, 32)], pub_json=True) - elif "/chargemode/instant_charging/dc_current" in msg.topic: - self._validate_value(msg, float, [(4, 300)], pub_json=True) - elif "/chargemode/instant_charging/limit/selected" in msg.topic: - self._validate_value(msg, str, pub_json=True) - elif "/chargemode/instant_charging/limit/soc" in msg.topic: - self._validate_value(msg, int, [(0, 100)], pub_json=True) - elif "/chargemode/instant_charging/limit/amount" in msg.topic: - self._validate_value(msg, int, [(1000, float("inf"))], pub_json=True) - elif "/chargemode/pv_charging/feed_in_limit" in msg.topic: - self._validate_value(msg, bool, pub_json=True) - elif "/chargemode/pv_charging/min_current" in msg.topic: - self._validate_value( - msg, int, [(0, 0), (6, 16)], pub_json=True) - elif "/chargemode/pv_charging/dc_min_current" in msg.topic: - self._validate_value(msg, float, [(0, 300)], pub_json=True) - elif "/chargemode/pv_charging/min_soc" in msg.topic: - self._validate_value(msg, int, [(0, 100)], pub_json=True) - elif "/chargemode/pv_charging/min_soc_current" in msg.topic: - self._validate_value(msg, int, [(6, 32)], pub_json=True) - elif "/chargemode/pv_charging/dc_min_soc_current" in msg.topic: - self._validate_value(msg, float, [(4, 300)], pub_json=True) - elif "/chargemode/pv_charging/max_soc" in msg.topic: - self._validate_value(msg, int, [(0, 101)], pub_json=True) - elif "/chargemode/scheduled_charging/plans/" in msg.topic and "/active" in msg.topic: - self._validate_value(msg, bool, pub_json=True) - elif "/chargemode/scheduled_charging/plans" in msg.topic: - self._validate_value(msg, "json") - elif "/chargemode/scheduled_charging" in msg.topic: - self._validate_value(msg, "json", pub_json=True) - elif "/et/active" in msg.topic: - self._validate_value(msg, bool, pub_json=True) - elif "/et/max_price" in msg.topic: - self._validate_value(msg, float, pub_json=True) - elif "/time_charging/active" in msg.topic: - self._validate_value(msg, bool, pub_json=True) - elif "/time_charging/plans/" in msg.topic and "/active" in msg.topic: - self._validate_value(msg, bool, pub_json=True) - elif "/time_charging/plans" in msg.topic: - self._validate_value(msg, "json") - else: - self._validate_value(msg, "json") - elif "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_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) @@ -531,7 +445,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): @@ -583,11 +498,10 @@ 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_chargemode_changed" in msg.topic or "/control_parameter/timestamp_last_phase_switch" in msg.topic): self._validate_value(msg, float, [(0, float("inf"))]) elif "/control_parameter/state" in msg.topic: @@ -799,10 +713,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/pv_charging/phases_to_use" in msg.topic or - "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/subdata.py b/packages/helpermodules/subdata.py index 67bdf994ef..05e3f73578 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -72,7 +72,6 @@ class SubData: def __init__(self, event_ev_template: threading.Event, - event_charge_template: threading.Event, event_cp_config: threading.Event, event_module_update_completed: threading.Event, event_copy_data: threading.Event, @@ -80,8 +79,6 @@ def __init__(self, event_command_completed: threading.Event, event_subdata_initialized: threading.Event, event_vehicle_update_completed: threading.Event, - event_scheduled_charging_plan: threading.Event, - event_time_charging_plan: threading.Event, event_start_internal_chargepoint: threading.Event, event_stop_internal_chargepoint: threading.Event, event_update_config_completed: threading.Event, @@ -91,7 +88,6 @@ def __init__(self, event_modbus_server: threading.Event, event_restart_gpio: threading.Event,): self.event_ev_template = event_ev_template - self.event_charge_template = event_charge_template self.event_cp_config = event_cp_config self.event_module_update_completed = event_module_update_completed self.event_copy_data = event_copy_data @@ -99,8 +95,6 @@ def __init__(self, self.event_command_completed = event_command_completed self.event_subdata_initialized = event_subdata_initialized self.event_vehicle_update_completed = event_vehicle_update_completed - self.event_scheduled_charging_plan = event_scheduled_charging_plan - self.event_time_charging_plan = event_time_charging_plan self.event_start_internal_chargepoint = event_start_internal_chargepoint self.event_stop_internal_chargepoint = event_stop_internal_chargepoint self.event_update_config_completed = event_update_config_completed @@ -122,12 +116,7 @@ def on_connect(self, client: mqtt.Client, userdata, flags: dict, rc: int): """ subscribe topics """ client.subscribe([ - ("openWB/vehicle/set/#", 2), - ("openWB/vehicle/template/#", 2), - ("openWB/vehicle/+/+", 2), - ("openWB/vehicle/+/get/#", 2), - ("openWB/vehicle/+/soc_module/config", 2), - ("openWB/vehicle/+/set/#", 2), + ("openWB/vehicle/#", 2), ("openWB/chargepoint/#", 2), ("openWB/pv/#", 2), ("openWB/bat/#", 2), @@ -314,11 +303,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 @@ -330,47 +331,81 @@ def process_vehicle_charge_template_topic(self, var: Dict[str, ev.ChargeTemplate """ try: index = get_index(msg.topic) - if decode_payload(msg.payload) == "" and re.search("/vehicle/template/charge_template/[0-9]+$", - msg.topic) is not None: - if "ct"+index in var: - 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)) - 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)) - self.event_time_charging_plan.set() + if re.search("/vehicle/template/charge_template/[0-9]+$", msg.topic) is not None: + if decode_payload(msg.payload) == "": + if "ct"+index in var: + var.pop("ct"+index) + if "ct"+index not in var: + var["ct"+index] = ChargeTemplate() + self.process_charge_template_topic(var["ct"+index], msg) + if re.search("/vehicle/template/charge_template/[0-9]+", msg.topic) is not None: + # 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): + # UI sendet immer alle Topics, auch nicht geänderte. Damit die temporären Topics nicht + # mehrfach gepbulished werden, muss das publishen der temporären Topics 1:1 erfolgen. + if re.search("/vehicle/template/charge_template/[0-9]+$", msg.topic) is not None: + if decode_payload(msg.payload) == "": + Pub().pub(f"openWB/chargepoint/{cp.chargepoint.num}/charge_template", "") + else: + cp.chargepoint.update_bare_charge_template(var["ct"+index]) + elif re.search("/vehicle/template/charge_template/[0-9]+/chargemode/scheduled_charging/" + "plans/[0-9]+", msg.topic) is not None: + plan_id = get_second_index(msg.topic) + if decode_payload(msg.payload) == "": + Pub().pub(f"openWB/chargepoint/{cp.chargepoint.num}/set/charge_template/" + f"chargemode/scheduled_charging/plans/{plan_id}", "") + else: + cp.chargepoint.update_charge_template_scheduled_plan( + var["ct"+index].data.chargemode.scheduled_charging.plans[plan_id]) + elif re.search("/vehicle/template/charge_template/[0-9]+/time_charging/plans/[0-9]+", + msg.topic) is not None: + plan_id = get_second_index(msg.topic) + if decode_payload(msg.payload) == "": + Pub().pub( + f"openWB/chargepoint/{cp.chargepoint.num}/set/charge_template/" + f"time_charging/plans/{plan_id}", "") + else: + cp.chargepoint.update_charge_template_time_plan( + var["ct"+index].data.time_charging.plans[plan_id]) + 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: - # 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() + 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") @@ -392,7 +427,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: @@ -412,11 +447,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( @@ -432,7 +468,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) @@ -604,14 +644,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: - 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/timecheck.py b/packages/helpermodules/timecheck.py index 02b116dec1..d3767fdfb5 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, 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. @@ -123,71 +122,53 @@ def check_duration(plan: ScheduledChargingPlan, duration: float, buffer: int) -> 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 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 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 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 + 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}') - 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 chargemode_switch_timestamp and end.timestamp() < chargemode_switch_timestamp: + # Als auf Zielladen umgeschaltet wurde, war der Termin schon vorbei + return None + else: + return remaining_time.total_seconds() + + +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..c42e76c279 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,54 +19,42 @@ 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"), - 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, chargemode_switch_timestamp=datetime.datetime.strptime("5/16/2022 8:00", "%m/%d/%Y %H:%M").timestamp()) # 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), ] ) @@ -79,23 +66,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"], diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 5d436a7f51..fe387efcf4 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 @@ -17,6 +18,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 +36,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 @@ -54,7 +56,7 @@ class UpdateConfig: - DATASTORE_VERSION = 80 + DATASTORE_VERSION = 83 valid_topic = [ "^openWB/bat/config/configured$", @@ -103,6 +105,7 @@ class UpdateConfig: "^openWB/chargepoint/[0-9]+/control_parameter/state$", "^openWB/chargepoint/[0-9]+/control_parameter/submode$", "^openWB/chargepoint/[0-9]+/control_parameter/timestamp_charge_start$", + "^openWB/chargepoint/[0-9]+/control_parameter/timestamp_chargemode_changed$", "^openWB/chargepoint/[0-9]+/control_parameter/timestamp_last_phase_switch$", "^openWB/chargepoint/[0-9]+/control_parameter/timestamp_switch_on_off$", "^openWB/chargepoint/[0-9]+/get/charge_state$", @@ -137,6 +140,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$", @@ -223,17 +229,12 @@ 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$", "^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$", - "^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$", "^openWB/general/prices/bat$", @@ -512,17 +513,16 @@ 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", 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), @@ -536,12 +536,8 @@ 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), - ("openWB/general/chargemode_config/scheduled_charging/phases_to_use_pv", 0), - ("openWB/general/chargemode_config/time_charging/phases_to_use", 1), + ChargemodeConfig().retry_failed_phase_switches), ("openWB/general/chargemode_config/unbalanced_load", False), ("openWB/general/chargemode_config/unbalanced_load_limit", 18), ("openWB/general/control_interval", 10), @@ -913,7 +909,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) @@ -2130,3 +2126,111 @@ def upgrade(topic: str, payload) -> Optional[dict]: f"{component_config['id']}/simulation"), "") self._loop_all_received_topics(upgrade) self.__update_topic("openWB/system/datastore_version", 80) + + def upgrade_datastore_80(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", 81) + + def upgrade_datastore_81(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", 82) + + def upgrade_datastore_82(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) + 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["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" + 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["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}) + + 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: + 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.items(): + if re.search(f"openWB/vehicle/template/charge_template/{index}" + "/time_charging/plans/[0-9]+$", time_plan_topic) is not None: + 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}) + + 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", 83) diff --git a/packages/main.py b/packages/main.py index aff4868442..c6ccb2f096 100755 --- a/packages/main.py +++ b/packages/main.py @@ -199,14 +199,8 @@ def schedule_jobs(): rfid = RfidReader() event_ev_template = threading.Event() event_ev_template.set() - event_charge_template = threading.Event() - event_charge_template.set() event_cp_config = threading.Event() event_cp_config.set() - event_scheduled_charging_plan = threading.Event() - event_scheduled_charging_plan.set() - event_time_charging_plan = threading.Event() - event_time_charging_plan.set() event_soc = threading.Event() event_soc.set() event_copy_data = threading.Event() # set: Kopieren abgeschlossen, reset: es wird kopiert @@ -224,14 +218,13 @@ def schedule_jobs(): gpio = InternalGpioHandler(event_restart_gpio) prep = prepare.Prepare() soc = update_soc.UpdateSoc(event_update_soc) - set = setdata.SetData(event_ev_template, event_charge_template, - event_cp_config, event_scheduled_charging_plan, event_time_charging_plan, event_soc, + set = setdata.SetData(event_ev_template, + event_cp_config, event_soc, event_subdata_initialized) - sub = subdata.SubData(event_ev_template, event_charge_template, + sub = subdata.SubData(event_ev_template, event_cp_config, loadvars_.event_module_update_completed, event_copy_data, event_global_data_initialized, event_command_completed, event_subdata_initialized, soc.event_vehicle_update_completed, - event_scheduled_charging_plan, event_time_charging_plan, general_internal_chargepoint_handler.event_start, general_internal_chargepoint_handler.event_stop, event_update_config_completed, diff --git a/packages/modules/display_themes/cards/config.py b/packages/modules/display_themes/cards/config.py index 0d4e528838..7cf8475142 100644 --- a/packages/modules/display_themes/cards/config.py +++ b/packages/modules/display_themes/cards/config.py @@ -9,6 +9,8 @@ class CardsDisplayThemeConfiguration: def __init__(self, lock_changes: bool = False, lock_changes_code: Optional[str] = None, + default_view: str = "dashboard", + default_view_timeout: int = 0, enable_dashboard_view: bool = True, enable_dashboard_card_grid: bool = True, enable_dashboard_card_home_consumption: bool = True, @@ -24,6 +26,8 @@ def __init__(self, self.lock_changes = lock_changes self.lock_changes_code = lock_changes_code # dashboard settings + self.default_view = default_view + self.default_view_timeout = default_view_timeout self.enable_dashboard_view = enable_dashboard_view self.enable_dashboard_card_grid = enable_dashboard_card_grid self.enable_dashboard_card_home_consumption = enable_dashboard_card_home_consumption diff --git a/packages/modules/display_themes/cards/source/package-lock.json b/packages/modules/display_themes/cards/source/package-lock.json index cfe1a0ca34..142991f864 100644 --- a/packages/modules/display_themes/cards/source/package-lock.json +++ b/packages/modules/display_themes/cards/source/package-lock.json @@ -14,11 +14,16 @@ "@fortawesome/vue-fontawesome": "^3.0.8", "@inkline/inkline": "^3.2.2", "buffer": "^6.0.3", + "chart.js": "^4.4.8", + "chartjs-adapter-luxon": "^1.3.1", + "chartjs-plugin-annotation": "^3.1.0", "events": "^3.3.0", + "luxon": "^3.5.0", "mqtt": "^5.10.4", "node-stdlib-browser": "^1.3.1", "pinia": "^2.3.1", "vue": "^3.5.13", + "vue-chartjs": "^5.3.2", "vue-router": "^4.5.0" }, "devDependencies": { @@ -2018,6 +2023,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@one-ini/wasm": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", @@ -3690,6 +3701,37 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz", + "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-adapter-luxon": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.3.1.tgz", + "integrity": "sha512-yxHov3X8y+reIibl1o+j18xzrcdddCLqsXhriV2+aQ4hCR66IYFchlRXUvrJVoxglJ380pgytU7YWtoqdIgqhg==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=3.0.0", + "luxon": ">=1.0.0" + } + }, + "node_modules/chartjs-plugin-annotation": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.1.0.tgz", + "integrity": "sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=4.0.0" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -5755,6 +5797,15 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -8654,6 +8705,16 @@ } } }, + "node_modules/vue-chartjs": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.2.tgz", + "integrity": "sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "vue": "^3.0.0-0 || ^2.7.0" + } + }, "node_modules/vue-component-type-helpers": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.8.tgz", diff --git a/packages/modules/display_themes/cards/source/package.json b/packages/modules/display_themes/cards/source/package.json index 9393fc8128..e3f7354de3 100644 --- a/packages/modules/display_themes/cards/source/package.json +++ b/packages/modules/display_themes/cards/source/package.json @@ -18,11 +18,16 @@ "@fortawesome/vue-fontawesome": "^3.0.8", "@inkline/inkline": "^3.2.2", "buffer": "^6.0.3", + "chart.js": "^4.4.8", + "chartjs-adapter-luxon": "^1.3.1", + "chartjs-plugin-annotation": "^3.1.0", "events": "^3.3.0", + "luxon": "^3.5.0", "mqtt": "^5.10.4", "node-stdlib-browser": "^1.3.1", "pinia": "^2.3.1", "vue": "^3.5.13", + "vue-chartjs": "^5.3.2", "vue-router": "^4.5.0" }, "devDependencies": { diff --git a/packages/modules/display_themes/cards/source/src/App.vue b/packages/modules/display_themes/cards/source/src/App.vue index ef178a6be1..fbab7e3b60 100644 --- a/packages/modules/display_themes/cards/source/src/App.vue +++ b/packages/modules/display_themes/cards/source/src/App.vue @@ -39,6 +39,9 @@ export default { "openWB/chargepoint/+/config", "openWB/chargepoint/+/get/charge_state", "openWB/chargepoint/+/get/connected_vehicle/+", + "openWB/chargepoint/+/set/charge_template", + "openWB/chargepoint/+/set/charge_template/chargemode/scheduled_charging/plans/+", + "openWB/chargepoint/+/set/charge_template/time_charging/plans/+", "openWB/chargepoint/+/get/phases_in_use", "openWB/chargepoint/+/get/plug_state", "openWB/chargepoint/+/get/power", @@ -51,6 +54,7 @@ export default { "openWB/counter/+/get/power", "openWB/counter/get/hierarchy", "openWB/counter/set/home_consumption", + "openWB/optional/et/get/prices", "openWB/optional/int_display/theme", "openWB/optional/int_display/standby", "openWB/optional/rfid/active", @@ -64,7 +68,6 @@ export default { "openWB/vehicle/+/get/fault_state", "openWB/vehicle/+/name", "openWB/vehicle/+/soc_module/config", - "openWB/vehicle/template/charge_template/#", ], mqttStore: useMqttStore(), chartInterval: "", diff --git a/packages/modules/display_themes/cards/source/src/components/ChargePoints/ChargeModeModal.vue b/packages/modules/display_themes/cards/source/src/components/ChargePoints/ChargeModeModal.vue new file mode 100644 index 0000000000..0035905721 --- /dev/null +++ b/packages/modules/display_themes/cards/source/src/components/ChargePoints/ChargeModeModal.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/packages/modules/display_themes/cards/source/src/components/ChargePoints/ChargePointCard.vue b/packages/modules/display_themes/cards/source/src/components/ChargePoints/ChargePointCard.vue index b39692b01b..eb5247058d 100644 --- a/packages/modules/display_themes/cards/source/src/components/ChargePoints/ChargePointCard.vue +++ b/packages/modules/display_themes/cards/source/src/components/ChargePoints/ChargePointCard.vue @@ -1,6 +1,6 @@ diff --git a/packages/modules/display_themes/cards/source/src/components/DashBoard/BatteryCard.vue b/packages/modules/display_themes/cards/source/src/components/Dashboard/BatteryCard.vue similarity index 86% rename from packages/modules/display_themes/cards/source/src/components/DashBoard/BatteryCard.vue rename to packages/modules/display_themes/cards/source/src/components/Dashboard/BatteryCard.vue index bf52149aca..d9d9469d43 100644 --- a/packages/modules/display_themes/cards/source/src/components/DashBoard/BatteryCard.vue +++ b/packages/modules/display_themes/cards/source/src/components/Dashboard/BatteryCard.vue @@ -1,6 +1,6 @@ diff --git a/packages/modules/display_themes/cards/source/src/components/DashBoard/ChargePointsCard.vue b/packages/modules/display_themes/cards/source/src/components/Dashboard/ChargePointsCard.vue similarity index 93% rename from packages/modules/display_themes/cards/source/src/components/DashBoard/ChargePointsCard.vue rename to packages/modules/display_themes/cards/source/src/components/Dashboard/ChargePointsCard.vue index 8054750f16..fbc5c08679 100644 --- a/packages/modules/display_themes/cards/source/src/components/DashBoard/ChargePointsCard.vue +++ b/packages/modules/display_themes/cards/source/src/components/Dashboard/ChargePointsCard.vue @@ -1,6 +1,6 @@ diff --git a/packages/modules/display_themes/cards/source/src/components/DashBoard/FlowCard.vue b/packages/modules/display_themes/cards/source/src/components/Dashboard/FlowCard.vue similarity index 87% rename from packages/modules/display_themes/cards/source/src/components/DashBoard/FlowCard.vue rename to packages/modules/display_themes/cards/source/src/components/Dashboard/FlowCard.vue index 1df7f1492d..7114e3fc40 100644 --- a/packages/modules/display_themes/cards/source/src/components/DashBoard/FlowCard.vue +++ b/packages/modules/display_themes/cards/source/src/components/Dashboard/FlowCard.vue @@ -1,11 +1,13 @@ diff --git a/packages/modules/display_themes/cards/source/src/components/NavBar.vue b/packages/modules/display_themes/cards/source/src/components/NavBar.vue index aecf8ed47a..a569e02dcd 100644 --- a/packages/modules/display_themes/cards/source/src/components/NavBar.vue +++ b/packages/modules/display_themes/cards/source/src/components/NavBar.vue @@ -20,8 +20,8 @@ export default { size="lg" > Übersicht diff --git a/packages/modules/display_themes/cards/source/src/components/TouchBlocker.vue b/packages/modules/display_themes/cards/source/src/components/TouchBlocker.vue index 66ebfa2ad6..aefba3ce4f 100644 --- a/packages/modules/display_themes/cards/source/src/components/TouchBlocker.vue +++ b/packages/modules/display_themes/cards/source/src/components/TouchBlocker.vue @@ -7,8 +7,10 @@ export default { return { mqttStore: useMqttStore(), show: false, - countdown: undefined, - countdownInterval: undefined, + touchBlockerCountdown: undefined, + touchBlockerCountdownInterval: undefined, + defaultViewCountdown: undefined, + defaultViewCountdownInterval: undefined, events: ["mousemove", "touchmove", "wheel", "click"], eventHandlerSetup: false, }; @@ -20,25 +22,38 @@ export default { } return this.mqttStore.getDisplayStandby; }, + configuredDefaultViewTimeout() { + if (this.mqttStore.getDefaultViewTimeout === 0 || this.mqttStore.getDefaultViewTimeout === undefined) { + return undefined; + } + return this.mqttStore.getDefaultViewTimeout; + }, touchBlockerTimeout() { // show touch blocker right before the configured standby time return Math.max(this.configuredDisplayStandby - 3, 1); }, + defaultViewTimeout() { + // switch to default view after the configured timeout + return this.configuredDefaultViewTimeout; + }, }, mounted() { this.setupEventHandler(); - this.setupTimeout(); + this.setupTouchBlockerTimeout(); + this.setupDefaultViewTimeout(); }, unmounted() { this.clearEventHandler(); - this.clearTimeout(); + this.clearTouchBlockerTimeout(); + this.clearDefaultViewTimeout(); }, methods: { handleTouchBlockerClick(event) { if (event === false) { this.show = false; this.setupEventHandler(); - this.setupTimeout(); + this.setupTouchBlockerTimeout(); + this.setupDefaultViewTimeout(); } }, setupEventHandler() { @@ -57,34 +72,72 @@ export default { this.eventHandlerSetup = false; } }, - setupTimeout() { - if (this.countdownInterval === undefined) { - this.countdownInterval = setInterval(this.updateCountdown, 1000); + setupTouchBlockerTimeout() { + if (this.touchBlockerCountdownCountdownInterval === undefined) { + this.touchBlockerCountdownCountdownInterval = setInterval(this.updateTouchBlockerCountdown, 1000); } }, - clearTimeout() { - if (this.countdownInterval !== undefined) { - clearInterval(this.countdownInterval); - this.countdownInterval = undefined; + clearTouchBlockerTimeout() { + if (this.touchBlockerCountdownCountdownInterval !== undefined) { + clearInterval(this.touchBlockerCountdownCountdownInterval); + this.touchBlockerCountdownCountdownInterval = undefined; } }, - updateCountdown() { - if (this.countdown === undefined) { - this.countdown = this.touchBlockerTimeout; + updateTouchBlockerCountdown() { + if (this.touchBlockerCountdown === undefined) { + this.touchBlockerCountdown = this.touchBlockerTimeout; } else { - this.countdown -= 1; - if (this.countdown < 1) { + this.touchBlockerCountdown -= 1; + if (this.touchBlockerCountdown < 1) { this.showTouchBlocker(); } } }, + setupDefaultViewTimeout() { + if ( + this.defaultViewCountdownInterval === undefined + && this.mqttStore.getDefaultView !== this.$route.name + && this.defaultViewTimeout !== undefined + ) { + this.defaultViewCountdownInterval = setInterval(this.updateDefaultViewCountdown, 1000); + } + }, + clearDefaultViewTimeout() { + if (this.defaultViewCountdownInterval !== undefined) { + clearInterval(this.defaultViewCountdownInterval); + this.defaultViewCountdownInterval = undefined; + } + }, + updateDefaultViewCountdown() { + if (this.defaultViewCountdown === undefined && this.defaultViewTimeout !== undefined) { + this.defaultViewCountdown = this.defaultViewTimeout; + } else { + if (this.$route.name === this.mqttStore.getDefaultView) { + this.clearDefaultViewTimeout(); + } else { + this.defaultViewCountdown -= 1; + if (this.defaultViewCountdown < 1) { + this.showDefaultView(); + } + } + } + }, handleDocumentEvent() { - this.countdown = this.touchBlockerTimeout; + this.touchBlockerCountdown = this.touchBlockerTimeout; + this.defaultViewCountdown = this.defaultViewTimeout; + this.setupDefaultViewTimeout(); this.show = false; }, showTouchBlocker() { this.show = true; - this.clearTimeout(); + this.clearTouchBlockerTimeout(); + }, + showDefaultView() { + console.log("switching to default view:", this.mqttStore.getDefaultView); + this.clearDefaultViewTimeout(); + if (this.$route.name !== this.mqttStore.getDefaultView) { + this.$router.push({ name: this.mqttStore.getDefaultView }); + } }, }, }; diff --git a/packages/modules/display_themes/cards/source/src/router/index.js b/packages/modules/display_themes/cards/source/src/router/index.js index 0dfa8970ff..d047715e98 100644 --- a/packages/modules/display_themes/cards/source/src/router/index.js +++ b/packages/modules/display_themes/cards/source/src/router/index.js @@ -10,12 +10,12 @@ const router = createRouter({ component: WelcomeView, }, { - path: "/DashBoard", - name: "dash-board", + path: "/Dashboard", + name: "dashboard", // route level code-splitting - // this generates a separate chunk (DashBoardView.[hash].js) for this route + // this generates a separate chunk (DashboardView.[hash].js) for this route // which is lazy-loaded when the route is visited. - component: () => import("../views/DashBoardView.vue"), + component: () => import("../views/DashboardView.vue"), }, { path: "/EnergyFlow", diff --git a/packages/modules/display_themes/cards/source/src/stores/mqtt.js b/packages/modules/display_themes/cards/source/src/stores/mqtt.js index e28790680c..e052c53b25 100644 --- a/packages/modules/display_themes/cards/source/src/stores/mqtt.js +++ b/packages/modules/display_themes/cards/source/src/stores/mqtt.js @@ -190,7 +190,36 @@ export const useMqttStore = defineStore("mqtt", { } return undefined; }, - getDashBoardEnabled(state) { + getDefaultView: (state) => { + if (state.getThemeConfiguration) { + const views = { + 'dashboard': state.getThemeConfiguration.enable_dashboard_view, + 'energy-flow': state.getThemeConfiguration.enable_energy_flow_view, + 'charge-points': state.getThemeConfiguration.enable_charge_points_view, + 'status': state.getThemeConfiguration.enable_status_view, + }; + if (state.getThemeConfiguration.default_view !== undefined) { + if (views[state.getThemeConfiguration.default_view] === true) { + return state.getThemeConfiguration.default_view; + } else { + console.warn(`default view '${state.getThemeConfiguration.default_view}' is not enabled, check your configuration!`); + } + } + for (const [view, enabled] of Object.entries(views)) { + if (enabled) { + return view; + } + } + } + return undefined; + }, + getDefaultViewTimeout: (state) => { + if (state.getThemeConfiguration) { + return state.getThemeConfiguration.default_view_timeout; + } + return 0; + }, + getDashboardEnabled(state) { if (state.getThemeConfiguration) { return state.getThemeConfiguration.enable_dashboard_view; } @@ -202,7 +231,6 @@ export const useMqttStore = defineStore("mqtt", { } return true; }, - getChargePointsEnabled(state) { if (state.getThemeConfiguration) { return state.getThemeConfiguration.enable_charge_points_view; @@ -608,12 +636,8 @@ export const useMqttStore = defineStore("mqtt", { }, getChargePointConnectedVehicleChargeTemplate(state) { return (chargePointId) => { - let chargeTemplateId = - state.getChargePointConnectedVehicleChargeTemplateIndex( - chargePointId, - ); return state.topics[ - `openWB/vehicle/template/charge_template/${chargeTemplateId}` + `openWB/chargepoint/${chargePointId}/set/charge_template` ]; }; }, @@ -689,6 +713,16 @@ export const useMqttStore = defineStore("mqtt", { return { selected: undefined }; }; }, + getChargePointConnectedVehicleInstantChargingPhases(state) { + return (chargePointId) => { + if (state.getChargePointConnectedVehicleChargeTemplate(chargePointId)) { + return state.getChargePointConnectedVehicleChargeTemplate( + chargePointId, + ).chargemode.instant_charging.phases_to_use; + } + return undefined; + }; + }, getChargePointConnectedVehiclePvChargingFeedInLimit(state) { return (chargePointId) => { if (state.getChargePointConnectedVehicleChargeTemplate(chargePointId)) { @@ -709,6 +743,26 @@ export const useMqttStore = defineStore("mqtt", { return undefined; }; }, + getChargePointConnectedVehiclePvChargingPhases(state) { + return (chargePointId) => { + if (state.getChargePointConnectedVehicleChargeTemplate(chargePointId)) { + return state.getChargePointConnectedVehicleChargeTemplate( + chargePointId, + ).chargemode.pv_charging.phases_to_use; + } + return undefined; + }; + }, + getChargePointConnectedVehiclePvChargingLimit(state) { + return (chargePointId) => { + if (state.getChargePointConnectedVehicleChargeTemplate(chargePointId)) { + return state.getChargePointConnectedVehicleChargeTemplate( + chargePointId, + ).chargemode.pv_charging.limit; + } + return { selected: undefined }; + }; + }, getChargePointConnectedVehiclePvChargingMinSoc(state) { return (chargePointId) => { if (state.getChargePointConnectedVehicleChargeTemplate(chargePointId)) { @@ -729,35 +783,67 @@ export const useMqttStore = defineStore("mqtt", { return undefined; }; }, - getChargePointConnectedVehiclePvChargingMaxSoc(state) { + getChargePointConnectedVehiclePvChargingMinSocPhases(state) { return (chargePointId) => { if (state.getChargePointConnectedVehicleChargeTemplate(chargePointId)) { return state.getChargePointConnectedVehicleChargeTemplate( chargePointId, - ).chargemode.pv_charging.max_soc; + ).chargemode.pv_charging.phases_to_use_min_soc; } return undefined; }; }, - getChargePointConnectedVehicleScheduledChargingPlans(state) { + getChargePointConnectedVehicleEcoChargingCurrent(state) { return (chargePointId) => { - let chargeTemplateId = - state.getChargePointConnectedVehicleChargeTemplateIndex( + if (state.getChargePointConnectedVehicleChargeTemplate(chargePointId)) { + return state.getChargePointConnectedVehicleChargeTemplate( chargePointId, - ); + ).chargemode.eco_charging.current; + } + return undefined; + }; + }, + getChargePointConnectedVehicleEcoChargingPhases(state) { + return (chargePointId) => { + if (state.getChargePointConnectedVehicleChargeTemplate(chargePointId)) { + return state.getChargePointConnectedVehicleChargeTemplate( + chargePointId, + ).chargemode.eco_charging.phases_to_use; + } + return undefined; + }; + }, + getChargePointConnectedVehicleEcoChargingLimit(state) { + return (chargePointId) => { + if (state.getChargePointConnectedVehicleChargeTemplate(chargePointId)) { + return state.getChargePointConnectedVehicleChargeTemplate( + chargePointId, + ).chargemode.eco_charging.limit; + } + return { selected: undefined }; + }; + }, + getChargePointConnectedVehicleEcoChargingMaxPrice(state) { + return (chargePointId) => { + if (state.getChargePointConnectedVehicleChargeTemplate(chargePointId)) { + return state.getChargePointConnectedVehicleChargeTemplate( + chargePointId, + ).chargemode.eco_charging.max_price * 100000; + } + return undefined; + }; + }, + getChargePointConnectedVehicleScheduledChargingPlans(state) { + return (chargePointId) => { return state.getWildcardTopics( - `openWB/vehicle/template/charge_template/${chargeTemplateId}/chargemode/scheduled_charging/plans/+`, + `openWB/chargepoint/${chargePointId}/set/charge_template/chargemode/scheduled_charging/plans/+`, ); }; }, getChargePointConnectedVehicleTimeChargingPlans(state) { return (chargePointId) => { - let chargeTemplateId = - state.getChargePointConnectedVehicleChargeTemplateIndex( - chargePointId, - ); return state.getWildcardTopics( - `openWB/vehicle/template/charge_template/${chargeTemplateId}/time_charging/plans/+`, + `openWB/chargepoint/${chargePointId}/set/charge_template/time_charging/plans/+`, ); }; }, @@ -848,6 +934,11 @@ export const useMqttStore = defineStore("mqtt", { getRfidEnabled() { return this.getValueBool("openWB/optional/rfid/active"); }, + + /* electricity tariff provider */ + getEtPrices(state) { + return state.topics["openWB/optional/et/get/prices"]; + }, }, actions: { updateSetting(setting, value) { @@ -904,8 +995,10 @@ export const useMqttStore = defineStore("mqtt", { } else { this.topics[topic] = payload; } + return this.topics[topic]; } else { console.debug("topic not found: ", topic); + return undefined; } }, updateChartData() { @@ -929,14 +1022,14 @@ export const useMqttStore = defineStore("mqtt", { }, updateState(topic, value, objectPath = undefined) { console.debug("updateState:", topic, value, objectPath); - this.updateTopic(topic, value, objectPath); + return this.updateTopic(topic, value, objectPath); }, chargeModeList() { var chargeModes = [ { id: "instant_charging" }, { id: "pv_charging" }, { id: "scheduled_charging" }, - { id: "standby" }, + { id: "eco_charging" }, { id: "stop" }, ]; chargeModes.forEach((mode) => { @@ -952,11 +1045,11 @@ export const useMqttStore = defineStore("mqtt", { case "pv_charging": return { mode: mode, label: "PV", class: "success" }; case "scheduled_charging": - return { mode: mode, label: "Zielladen", class: "primary" }; + return { mode: mode, label: "Ziel", class: "primary" }; case "time_charging": - return { mode: mode, label: "Zeitladen", class: "warning" }; - case "standby": - return { mode: mode, label: "Standby", class: "secondary" }; + return { mode: mode, label: "Zeit", class: "warning" }; + case "eco_charging": + return { mode: mode, label: "Eco", class: "secondary" }; case "stop": return { mode: mode, label: "Stop", class: "dark" }; default: @@ -986,6 +1079,7 @@ export const useMqttStore = defineStore("mqtt", { const beginDate = new Date(dateArray[0]); const endDate = new Date(dateArray[1]); if (beginDate.getFullYear() == endDate.getFullYear()) { + separator = `.${separator}`; if (beginDate.getMonth() != endDate.getMonth()) { // add display of month if different and year is identical beginFormat.month = endFormat.month; @@ -1000,14 +1094,37 @@ export const useMqttStore = defineStore("mqtt", { )}${separator}${this.formatDate(dateArray[1], endFormat)}`; }, formatWeeklyScheduleDays(weekDays) { - const days = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; - let planDays = []; - weekDays.forEach(function (dayValue, index) { - if (dayValue == true) { - planDays.push(days[index]); + const days = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; + let planDays = []; + let rangeStart = null; + + weekDays.forEach((dayValue, index) => { + if (dayValue) { + if (rangeStart === null) { + rangeStart = index; + } + } else { + if (rangeStart !== null) { + if (rangeStart === index - 1) { + planDays.push(days[rangeStart]); + } else { + planDays.push(`${days[rangeStart]}-${days[index - 1]}`); + } + rangeStart = null; + } + } + }); + + // Handle the case where the last day(s) of the week are true + if (rangeStart !== null) { + if (rangeStart === weekDays.length - 1) { + planDays.push(days[rangeStart]); + } else { + planDays.push(`${days[rangeStart]}-${days[weekDays.length - 1]}`); + } } - }); - return planDays.join(","); - }, + + return planDays.join(", "); + } }, }); diff --git a/packages/modules/display_themes/cards/source/src/views/ChargePointsView.vue b/packages/modules/display_themes/cards/source/src/views/ChargePointsView.vue index 39ad8a22ef..c50f43b02f 100644 --- a/packages/modules/display_themes/cards/source/src/views/ChargePointsView.vue +++ b/packages/modules/display_themes/cards/source/src/views/ChargePointsView.vue @@ -4,6 +4,9 @@ import ChargePointCard from "@/components/ChargePoints/ChargePointCard.vue"; import SimpleChargePointCard from "@/components/ChargePoints/SimpleChargePointCard.vue"; import ExtendedNumberInput from "@/components/ExtendedNumberInput.vue"; import ManualSocInput from "@/components/ChargePoints/ManualSocInput.vue"; +import ChargeModeModal from "../components/ChargePoints/ChargeModeModal.vue"; +import VehicleSelectModal from "../components/ChargePoints/VehicleSelectModal.vue"; +import ElectricityTariffChart from "../components/ElectricityTariffChart.vue"; /* fontawesome */ import { library } from "@fortawesome/fontawesome-svg-core"; @@ -13,6 +16,7 @@ import { faCalendarDay as fasCalendarDay, faCalendarWeek as fasCalendarWeek, faCalendarAlt as fasCalendarAlt, + faCoins as fasCoins, } from "@fortawesome/free-solid-svg-icons"; /* add icons to the library */ library.add( @@ -20,6 +24,7 @@ library.add( fasCalendarDay, fasCalendarWeek, fasCalendarAlt, + fasCoins, ); export default { @@ -29,7 +34,10 @@ export default { SimpleChargePointCard, ExtendedNumberInput, ManualSocInput, + ChargeModeModal, + VehicleSelectModal, FontAwesomeIcon, + ElectricityTariffChart, }, props: { changesLocked: { required: false, type: Boolean, default: false }, @@ -44,34 +52,16 @@ export default { modalVehicleId: 0, modalActiveTab: "tab-general", modalManualSocInputVisible: false, - simpleChargeModes: [ - "instant_charging", - "pv_charging", - "stop", - ] }; }, computed: { - vehicleList() { - let topicList = this.mqttStore.getVehicleList; - /* topicList is an object, but we need an array for our select input */ - var vehicleList = []; - Object.keys(topicList).forEach((topic) => { - let id = parseInt( - topic.match(/(?:\/)([0-9]+)(?=\/)*/g)[0].replace(/[^0-9]+/g, ""), - ); - vehicleList.push({ id: id, name: topicList[topic] }); - }); - return vehicleList; - }, - filteredChargeModes() { - if (this.mqttStore.getSimpleChargePointView) { - return this.mqttStore.chargeModeList().filter((mode) => { - return this.simpleChargeModes.includes(mode.id); - }); - } - return this.mqttStore.chargeModeList() - }, + timeChargingEnabled() { + return (chargePointId) => { + return this.mqttStore.getChargePointConnectedVehicleTimeChargingActive( + chargePointId, + ) === true; + }; + } }, watch: { changesLocked(newValue, oldValue) { @@ -96,6 +86,9 @@ export default { case "scheduled_charging": this.modalActiveTab = "tab-scheduled-charging"; break; + case "eco_charging": + this.modalActiveTab = "tab-eco-charging"; + break; default: this.modalActiveTab = "tab-instant-charging"; } @@ -126,38 +119,27 @@ export default { 1, ); }, - setChargePointConnectedVehicle(id, event) { - if (event.id != this.mqttStore.getChargePointConnectedVehicleId(id)) { - this.$root.sendTopicToBroker( - `openWB/chargepoint/${id}/config/ev`, - event.id, - ); - } - // hide modal vehicle select if visible - if (this.modalVehicleSelectVisible) { - this.modalVehicleSelectVisible = false; - } + updateChargePointChargeTemplate(chargePointId, newValue, objectPath = undefined) { + const chargeTemplate = this.mqttStore.updateState( + `openWB/chargepoint/${chargePointId}/set/charge_template`, + newValue, + objectPath, + ); + this.$root.sendTopicToBroker( + `openWB/chargepoint/${chargePointId}/set/charge_template`, + chargeTemplate, + ); }, setChargePointConnectedVehicleChargeMode(id, event) { if ( event.id != this.mqttStore.getChargePointConnectedVehicleChargeMode(id) ) { - var template_id = - this.mqttStore.getChargePointConnectedVehicleChargeTemplateIndex(id); - this.$root.sendTopicToBroker( - `openWB/vehicle/template/charge_template/${template_id}/chargemode/selected`, - event, - ); + this.updateChargePointChargeTemplate(id, event, "chargemode.selected"); } }, setChargePointConnectedVehiclePriority(id, event) { if (event != this.mqttStore.getChargePointConnectedVehiclePriority(id)) { - var template_id = - this.mqttStore.getChargePointConnectedVehicleChargeTemplateIndex(id); - this.$root.sendTopicToBroker( - `openWB/vehicle/template/charge_template/${template_id}/prio`, - event, - ); + this.updateChargePointChargeTemplate(id, event, "prio"); } }, setChargePointConnectedVehicleTimeChargingActive(id, event) { @@ -165,12 +147,7 @@ export default { event != this.mqttStore.getChargePointConnectedVehicleTimeChargingActive(id) ) { - var template_id = - this.mqttStore.getChargePointConnectedVehicleChargeTemplateIndex(id); - this.$root.sendTopicToBroker( - `openWB/vehicle/template/charge_template/${template_id}/time_charging/active`, - event, - ); + this.updateChargePointChargeTemplate(id, event, "time_charging.active"); } }, setChargePointConnectedVehicleInstantChargingCurrent(id, event) { @@ -181,57 +158,46 @@ export default { id, ) ) { - var template_id = - this.mqttStore.getChargePointConnectedVehicleChargeTemplateIndex(id); - this.$root.sendTopicToBroker( - `openWB/vehicle/template/charge_template/${template_id}/chargemode/instant_charging/current`, - parseFloat(event), - ); + this.updateChargePointChargeTemplate(id, event, "chargemode.instant_charging.current"); } }, - setChargePointConnectedVehicleInstantChargingLimit(id, selected_limit) { + setChargePointConnectedVehicleInstantChargingPhases(id, event) { if ( - selected_limit && - selected_limit != + event && + event != + this.mqttStore.getChargePointConnectedVehicleInstantChargingPhases(id) + ) { + this.updateChargePointChargeTemplate(id, event, "chargemode.instant_charging.phases_to_use"); + } + }, + setChargePointConnectedVehicleInstantChargingLimit(id, event) { + if ( + event && + event != this.mqttStore.getChargePointConnectedVehicleInstantChargingLimit(id) .selected ) { - var template_id = - this.mqttStore.getChargePointConnectedVehicleChargeTemplateIndex(id); - this.$root.sendTopicToBroker( - `openWB/vehicle/template/charge_template/${template_id}/chargemode/instant_charging/limit/selected`, - selected_limit, - ); + this.updateChargePointChargeTemplate(id, event, "chargemode.instant_charging.limit.selected"); } }, - setChargePointConnectedVehicleInstantChargingLimitSoc(id, soc_limit) { + setChargePointConnectedVehicleInstantChargingLimitSoc(id, event) { if ( - soc_limit && - soc_limit != + event && + event != this.mqttStore.getChargePointConnectedVehicleInstantChargingLimit(id) .soc ) { - var template_id = - this.mqttStore.getChargePointConnectedVehicleChargeTemplateIndex(id); - this.$root.sendTopicToBroker( - `openWB/vehicle/template/charge_template/${template_id}/chargemode/instant_charging/limit/soc`, - parseInt(soc_limit), - ); + this.updateChargePointChargeTemplate(id, parseInt(event), "chargemode.instant_charging.limit.soc"); } }, - setChargePointConnectedVehicleInstantChargingLimitAmount(id, amount_limit) { + setChargePointConnectedVehicleInstantChargingLimitAmount(id, event) { if ( - amount_limit && - amount_limit != + event && + event != this.mqttStore.getChargePointConnectedVehicleInstantChargingLimit(id) .amount ) { - var template_id = - this.mqttStore.getChargePointConnectedVehicleChargeTemplateIndex(id); - this.$root.sendTopicToBroker( - `openWB/vehicle/template/charge_template/${template_id}/chargemode/instant_charging/limit/amount`, - amount_limit, - ); + this.updateChargePointChargeTemplate(id, event, "chargemode.instant_charging.limit.amount"); } }, setChargePointConnectedVehiclePvChargingFeedInLimit(id, event) { @@ -239,12 +205,7 @@ export default { event != this.mqttStore.getChargePointConnectedVehiclePvChargingFeedInLimit(id) ) { - var template_id = - this.mqttStore.getChargePointConnectedVehicleChargeTemplateIndex(id); - this.$root.sendTopicToBroker( - `openWB/vehicle/template/charge_template/${template_id}/chargemode/pv_charging/feed_in_limit`, - event, - ); + this.updateChargePointChargeTemplate(id, event, "chargemode.pv_charging.feed_in_limit"); } }, setChargePointConnectedVehiclePvChargingMinCurrent(id, event) { @@ -252,12 +213,46 @@ export default { this.mqttStore.getChargePointConnectedVehiclePvChargingMinCurrent(id); let new_value = parseInt(event); if (new_value != previous_value && !isNaN(new_value)) { - var template_id = - this.mqttStore.getChargePointConnectedVehicleChargeTemplateIndex(id); - this.$root.sendTopicToBroker( - `openWB/vehicle/template/charge_template/${template_id}/chargemode/pv_charging/min_current`, - new_value, - ); + this.updateChargePointChargeTemplate(id, new_value, "chargemode.pv_charging.min_current"); + } + }, + setChargePointConnectedVehiclePvChargingPhases(id, selected_phases) { + if ( + selected_phases !== undefined && + selected_phases != + this.mqttStore.getChargePointConnectedVehiclePvChargingPhases(id) + ) { + this.updateChargePointChargeTemplate(id, selected_phases, "chargemode.pv_charging.phases_to_use"); + } + }, + setChargePointConnectedVehiclePvChargingLimit(id, selected_limit) { + if ( + selected_limit && + selected_limit != + this.mqttStore.getChargePointConnectedVehiclePvChargingLimit(id) + .selected + ) { + this.updateChargePointChargeTemplate(id, selected_limit, "chargemode.pv_charging.limit.selected"); + } + }, + setChargePointConnectedVehiclePvChargingLimitSoc(id, soc_limit) { + if ( + soc_limit && + soc_limit != + this.mqttStore.getChargePointConnectedVehiclePvChargingLimit(id) + .soc + ) { + this.updateChargePointChargeTemplate(id, parseInt(soc_limit), "chargemode.pv_charging.limit.soc"); + } + }, + setChargePointConnectedVehiclePvChargingLimitAmount(id, amount_limit) { + if ( + amount_limit && + amount_limit != + this.mqttStore.getChargePointConnectedVehiclePvChargingLimit(id) + .amount + ) { + this.updateChargePointChargeTemplate(id, amount_limit, "chargemode.pv_charging.limit.amount"); } }, setChargePointConnectedVehiclePvChargingMinSoc(id, soc) { @@ -265,12 +260,7 @@ export default { this.mqttStore.getChargePointConnectedVehiclePvChargingMinSoc(id); let new_value = parseInt(soc); if (new_value != previous_value && !isNaN(new_value)) { - var template_id = - this.mqttStore.getChargePointConnectedVehicleChargeTemplateIndex(id); - this.$root.sendTopicToBroker( - `openWB/vehicle/template/charge_template/${template_id}/chargemode/pv_charging/min_soc`, - new_value, - ); + this.updateChargePointChargeTemplate(id, new_value, "chargemode.pv_charging.min_soc"); } }, setChargePointConnectedVehiclePvChargingMinSocCurrent(id, event) { @@ -280,35 +270,91 @@ export default { ); let new_value = parseInt(event); if (new_value != previous_value && !isNaN(new_value)) { - var template_id = - this.mqttStore.getChargePointConnectedVehicleChargeTemplateIndex(id); - this.$root.sendTopicToBroker( - `openWB/vehicle/template/charge_template/${template_id}/chargemode/pv_charging/min_soc_current`, - new_value, - ); + this.updateChargePointChargeTemplate(id, new_value, "chargemode.pv_charging.min_soc_current"); + } + }, + setChargePointConnectedVehiclePvChargingMinSocPhases(id, selected_phases) { + if ( + selected_phases && + selected_phases != + this.mqttStore.getChargePointConnectedVehiclePvChargingMinSocPhases(id) + ) { + this.updateChargePointChargeTemplate(id, selected_phases, "chargemode.pv_charging.phases_to_use_min_soc"); } }, - setChargePointConnectedVehiclePvChargingMaxSoc(id, soc) { + setChargePointConnectedVehicleEcoChargingCurrent(id, event) { let previous_value = - this.mqttStore.getChargePointConnectedVehiclePvChargingMaxSoc(id); - let new_value = parseInt(soc); + this.mqttStore.getChargePointConnectedVehicleEcoChargingCurrent(id); + let new_value = parseInt(event); if (new_value != previous_value && !isNaN(new_value)) { - var template_id = - this.mqttStore.getChargePointConnectedVehicleChargeTemplateIndex(id); - this.$root.sendTopicToBroker( - `openWB/vehicle/template/charge_template/${template_id}/chargemode/pv_charging/max_soc`, - new_value, - ); + this.updateChargePointChargeTemplate(id, new_value, "chargemode.eco_charging.current"); + } + }, + setChargePointConnectedVehicleEcoChargingPhases(id, selected_phases) { + if ( + selected_phases !== undefined && + selected_phases != + this.mqttStore.getChargePointConnectedVehicleEcoChargingPhases(id) + ) { + this.updateChargePointChargeTemplate(id, selected_phases, "chargemode.eco_charging.phases_to_use"); + } + }, + setChargePointConnectedVehicleEcoChargingLimit(id, selected_limit) { + if ( + selected_limit && + selected_limit != + this.mqttStore.getChargePointConnectedVehicleEcoChargingLimit(id) + .selected + ) { + this.updateChargePointChargeTemplate(id, selected_limit, "chargemode.eco_charging.limit.selected"); + } + }, + setChargePointConnectedVehicleEcoChargingLimitSoc(id, soc_limit) { + if ( + soc_limit && + soc_limit != + this.mqttStore.getChargePointConnectedVehicleEcoChargingLimit(id) + .soc + ) { + this.updateChargePointChargeTemplate(id, parseInt(soc_limit), "chargemode.eco_charging.limit.soc"); + } + }, + setChargePointConnectedVehicleEcoChargingLimitAmount(id, amount_limit) { + if ( + amount_limit && + amount_limit != + this.mqttStore.getChargePointConnectedVehicleEcoChargingLimit(id) + .amount + ) { + this.updateChargePointChargeTemplate(id, amount_limit, "chargemode.eco_charging.limit.amount"); + } + }, + setChargePointConnectedVehicleEcoChargingMaxPrice(id, event) { + let previous_value = + this.mqttStore.getChargePointConnectedVehicleEcoChargingMaxPrice(id); + let new_value = parseFloat(event); + if (new_value != previous_value && !isNaN(new_value)) { + this.updateChargePointChargeTemplate(id, parseFloat((new_value / 100000).toFixed(7)), "chargemode.eco_charging.max_price"); } }, setChargePointConnectedVehicleScheduledChargingPlanActive( plan_key, active, ) { - this.$root.sendTopicToBroker(`${plan_key}/active`, active); + const plan = this.mqttStore.updateState( + `${plan_key}`, + active, + "active", + ); + this.$root.sendTopicToBroker(`${plan_key}`, plan); }, setChargePointConnectedVehicleTimeChargingPlanActive(plan_key, active) { - this.$root.sendTopicToBroker(`${plan_key}/active`, active); + const plan = this.mqttStore.updateState( + `${plan_key}`, + active, + "active", + ); + this.$root.sendTopicToBroker(`${plan_key}`, plan); }, }, }; @@ -331,126 +377,15 @@ export default { - - - - - - - {{ mode.label }} - - - - - Priorität - - - Nein - - - Ja - - - - - - + :charge-point-id="modalChargePointId" + /> - - - - - - - {{ vehicle.name }} - - - - - - + :charge-point-id="modalChargePointId" + /> PV + + Eco + - Zielladen + Ziel - Zeitladen + Zeit @@ -507,6 +448,55 @@ export default { " /> + + Anzahl Phasen + + + 1 + + + Maximum + + + Begrenzung @@ -530,7 +520,7 @@ export default { ) " > - Keine + Aus - - Einspeisegrenze beachten - - - Nein - - - Ja - - - Minimaler Dauerstrom - Mindest-SoC - - - - Mindest-SoC Strom - Anzahl Phasen + + + 1 + + + Maximum + + + Automatik + + + + + Begrenzung + + + Aus + + + EV-SoC + + + Energie + + + + + SoC-Limit für das Fahrzeug + + + Energie-Limit + + - SoC-Limit + Mindest-SoC für das Fahrzeug + + + Mindest-SoC Strom + + + + Anzahl Phasen Mindest-SoC + + + 1 + + + Maximum + + + + + Einspeisegrenze beachten + + + Nein + + + Ja + + + + + + + + + Minimaler Dauerstrom unter Preisgrenze + + + + Anzahl Phasen + + + 1 + + + Maximum + + + Automatik + + + + + Begrenzung + + + Aus + + + EV-SoC + + + Energie + + + + + SoC-Limit für das Fahrzeug + + + + Energie-Limit + + + + Preisgrenze für strompreisbasiertes Laden + + - - {{ plan.name }} - - - - {{ mqttStore.formatDate(plan.frequency.once) }} - - - - täglich - - - - {{ - mqttStore.formatWeeklyScheduleDays(plan.frequency.weekly) - }} - - - {{ plan.time }} - - - {{ plan.limit.soc_scheduled }} % - - - - {{ plan.limit.amount / 1000 }} kWh - +
+ {{ plan.name }} +
+
+
+ + {{ mqttStore.formatDate(plan.frequency.once) }} +
+
+ + täglich +
+
+ + {{ + mqttStore.formatWeeklyScheduleDays(plan.frequency.weekly) + }} +
+
+ + {{ plan.time }} +
+
+ + {{ plan.limit.soc_scheduled }} % +
+
+ + {{ plan.limit.amount / 1000 }} kWh +
+
+ +
+
@@ -902,14 +1350,12 @@ export default { name="tab-time-charging" > - + Zeitladen aktivieren -
- + + + Es wurden noch keine Zeitpläne für das Zeitladen eingerichtet. + +
+ - - Es wurden noch keine Zeitpläne für das Zeitladen eingerichtet. - -
- - - - {{ plan.name }} - - - - + + + +
+ {{ plan.name }} +
+
+
{{ mqttStore.formatDateRange(plan.frequency.once) }} - - +
+
täglich - - +
+
{{ @@ -1016,31 +1450,30 @@ export default { plan.frequency.weekly, ) }} - - - {{ plan.time.join("-") }} - +
+
+ + {{ plan.time.join("-") }} +
+
{{ plan.limit.soc }} % - - +
+
{{ plan.limit.amount / 1000 }} kWh - - - - - -
+
+
+ + + +
@@ -1052,7 +1485,6 @@ export default { v-model="modalManualSocInputVisible" :vehicle-id="modalVehicleId" /> - diff --git a/packages/modules/display_themes/cards/source/src/views/DashBoardView.vue b/packages/modules/display_themes/cards/source/src/views/DashboardView.vue similarity index 69% rename from packages/modules/display_themes/cards/source/src/views/DashBoardView.vue rename to packages/modules/display_themes/cards/source/src/views/DashboardView.vue index 0f4ef13d4a..442c501de0 100644 --- a/packages/modules/display_themes/cards/source/src/views/DashBoardView.vue +++ b/packages/modules/display_themes/cards/source/src/views/DashboardView.vue @@ -1,10 +1,10 @@ diff --git a/packages/modules/display_themes/cards/source/src/views/StatusView.vue b/packages/modules/display_themes/cards/source/src/views/StatusView.vue index 9ceb05c011..7190d61b2a 100644 --- a/packages/modules/display_themes/cards/source/src/views/StatusView.vue +++ b/packages/modules/display_themes/cards/source/src/views/StatusView.vue @@ -3,7 +3,7 @@ import { useMqttStore } from "@/stores/mqtt.js"; import ReloadButton from "@/components/Status/ReloadButton.vue"; import RebootButton from "@/components/Status/RebootButton.vue"; import ShutdownButton from "@/components/Status/ShutdownButton.vue"; -import DashBoardCard from "@/components/DashBoardCard.vue"; +import DashboardCard from "@/components/DashboardCard.vue"; export default { name: "StatusView", @@ -11,7 +11,7 @@ export default { ReloadButton, RebootButton, ShutdownButton, - DashBoardCard, + DashboardCard, }, props: { changesLocked: { required: false, type: Boolean, default: false }, @@ -25,7 +25,7 @@ export default { diff --git a/packages/modules/display_themes/cards/source/src/views/WelcomeView.vue b/packages/modules/display_themes/cards/source/src/views/WelcomeView.vue index 39d662d3f0..4602a676a0 100644 --- a/packages/modules/display_themes/cards/source/src/views/WelcomeView.vue +++ b/packages/modules/display_themes/cards/source/src/views/WelcomeView.vue @@ -8,32 +8,13 @@ export default { mqttStore: useMqttStore(), }; }, - computed: { - firstView() { - if (this.mqttStore.getThemeConfiguration) { - if (this.mqttStore.getThemeConfiguration.enable_dashboard_view) { - return "dash-board"; - } - if (this.mqttStore.getThemeConfiguration.enable_energy_flow_view) { - return "energy-flow"; - } - if (this.mqttStore.getThemeConfiguration.enable_charge_points_view) { - return "charge-points"; - } - if (this.mqttStore.getThemeConfiguration.enable_status_view) { - return "status"; - } - } - return undefined; - }, - }, mounted() { setTimeout(this.selectFirstRoute, 3000); }, methods: { selectFirstRoute() { - if (this.firstView) { - this.$router.push({ name: this.firstView }); + if (this.mqttStore.getDefaultView) { + this.$router.push({ name: this.mqttStore.getDefaultView }); } else { console.warn("no router view enabled, check your configuration!"); } diff --git a/packages/modules/update_soc_test.py b/packages/modules/update_soc_test.py index cbaee98aa1..74526e1c75 100644 --- a/packages/modules/update_soc_test.py +++ b/packages/modules/update_soc_test.py @@ -22,7 +22,7 @@ def mock_data() -> None: data.data_init(Mock()) - SubData(*([Mock()]*19)) + SubData(*([Mock()]*16)) SubData.cp_data = {"cp0": Mock(spec=ChargepointStateUpdate, chargepoint=Mock( spec=Chargepoint, id=id, diff --git a/packages/modules/web_themes/colors/source/src/assets/css/style.css b/packages/modules/web_themes/colors/source/src/assets/css/style.css index 5939cccb11..d98dbda359 100644 --- a/packages/modules/web_themes/colors/source/src/assets/css/style.css +++ b/packages/modules/web_themes/colors/source/src/assets/css/style.css @@ -196,6 +196,7 @@ body>.container-fluid { --font-verysmall: 0.7rem; --font-extralarge: 1.7rem; --font-settings: 16px; + --font-settings-button: 14px; } /* Theme color sets */ @@ -314,7 +315,11 @@ body>.container-fluid { gap: .2em; justify-items: center; } -.grid-col-3 { +.grid-col-1 { + grid-column: span 1; +}.grid-col-2 { + grid-column: span 2; +}.grid-col-3 { grid-column: span 3; } .grid-col-4 { diff --git a/packages/modules/web_themes/colors/source/src/assets/js/sendMessages.ts b/packages/modules/web_themes/colors/source/src/assets/js/sendMessages.ts index 14e680880d..27e227633d 100644 --- a/packages/modules/web_themes/colors/source/src/assets/js/sendMessages.ts +++ b/packages/modules/web_themes/colors/source/src/assets/js/sendMessages.ts @@ -45,6 +45,7 @@ const topics: { [topic: string]: string } = { socUpdate: 'openWB/set/vehicle/%/get/force_soc_update', setSoc: 'openWB/set/vehicle/%/soc_module/calculated_soc_state/manual_soc', priceCharging: 'openWB/set/vehicle/template/charge_template/%/et/active', + chargeTemplate: 'openWB/set/chargepoint/%/set/charge_template', } export function updateServer( item: string, @@ -61,7 +62,7 @@ export function updateServer( return } switch (item) { - case 'chargeMode': + /* case 'chargeMode': case 'cpPriority': case 'cpScheduledCharging': case 'cpInstantTargetCurrent': @@ -76,7 +77,7 @@ export function updateServer( case 'cpPvMinSocCurrent': // these values are set in the charge template topic = topic.replace('%', chargePoints[index].chargeTemplate.toString()) - break + break */ default: topic = topic.replace('%', String(index)) } @@ -96,3 +97,10 @@ export function sendCommand(event: object) { JSON.stringify(event), ) } + +export function updateChargeTemplate(cp: number) { + mqttPublish( + topics.chargeTemplate.replace('%', String(cp)), + JSON.stringify(chargePoints[cp].chargeTemplate), + ) +} diff --git a/packages/modules/web_themes/colors/source/src/assets/js/themeConfig.ts b/packages/modules/web_themes/colors/source/src/assets/js/themeConfig.ts index cf41622841..15fb212c61 100644 --- a/packages/modules/web_themes/colors/source/src/assets/js/themeConfig.ts +++ b/packages/modules/web_themes/colors/source/src/assets/js/themeConfig.ts @@ -362,7 +362,7 @@ export const chargemodes: { [key: string]: ChargeModeInfo } = { pv_charging: { mode: ChargeMode.pv_charging, name: 'PV', - color: 'var(--color-pv', + color: 'var(--color-pv)', icon: 'fa-solar-panel', }, scheduled_charging: { @@ -371,11 +371,11 @@ export const chargemodes: { [key: string]: ChargeModeInfo } = { color: 'var(--color-battery)', icon: 'fa-bullseye', }, - standby: { - mode: ChargeMode.standby, - name: 'Standby', - color: 'var(--color-axis', - icon: 'fa-pause', + eco_charging: { + mode: ChargeMode.eco_charging, + name: 'Eco', + color: 'var(--color-devices)', + icon: 'fa-coins', }, stop: { mode: ChargeMode.stop, diff --git a/packages/modules/web_themes/colors/source/src/assets/js/types.ts b/packages/modules/web_themes/colors/source/src/assets/js/types.ts index 06f354ed25..00164a9e16 100644 --- a/packages/modules/web_themes/colors/source/src/assets/js/types.ts +++ b/packages/modules/web_themes/colors/source/src/assets/js/types.ts @@ -33,7 +33,7 @@ export enum ChargeMode { instant_charging = 'instant_charging', pv_charging = 'pv_charging', scheduled_charging = 'scheduled_charging', - standby = 'standby', + eco_charging = 'eco_charging', stop = 'stop', } diff --git a/packages/modules/web_themes/colors/source/src/components/batteryList/BatteryList.vue b/packages/modules/web_themes/colors/source/src/components/batteryList/BatteryList.vue index c7d73f7623..653ba62a4b 100755 --- a/packages/modules/web_themes/colors/source/src/components/batteryList/BatteryList.vue +++ b/packages/modules/web_themes/colors/source/src/components/batteryList/BatteryList.vue @@ -36,7 +36,12 @@ Hagen */
- + diff --git a/packages/modules/web_themes/colors/source/src/components/buttonBar/BBSelect.vue b/packages/modules/web_themes/colors/source/src/components/buttonBar/BBSelect.vue index 9e1aafd653..59b3c3d648 100755 --- a/packages/modules/web_themes/colors/source/src/components/buttonBar/BBSelect.vue +++ b/packages/modules/web_themes/colors/source/src/components/buttonBar/BBSelect.vue @@ -122,7 +122,7 @@ const buttons = [ name: 'Zielladen', color: 'var(--color-battery)', }, - { mode: 'standby', name: 'Standby', color: 'var(--color-axis)' }, + { mode: 'eco_charging', name: 'Eco', color: 'var(--color-devices)' }, { mode: 'stop', name: 'Stop', color: 'var(--color-axis)' }, ] const cp = computed(() => { diff --git a/packages/modules/web_themes/colors/source/src/components/buttonBar/BbChargeButton.vue b/packages/modules/web_themes/colors/source/src/components/buttonBar/BbChargeButton.vue index fcfa6dcf5a..d275e00c64 100644 --- a/packages/modules/web_themes/colors/source/src/components/buttonBar/BbChargeButton.vue +++ b/packages/modules/web_themes/colors/source/src/components/buttonBar/BbChargeButton.vue @@ -100,7 +100,6 @@ const modePillStyle = computed(() => { style = swapcolors(style) } break - case ChargeMode.standby: case ChargeMode.stop: style.background = 'darkgrey' style.color = 'black' diff --git a/packages/modules/web_themes/colors/source/src/components/chargePointList/CPChargePoint.vue b/packages/modules/web_themes/colors/source/src/components/chargePointList/CPChargePoint.vue index 6a2648ce41..00552734b7 100755 --- a/packages/modules/web_themes/colors/source/src/components/chargePointList/CPChargePoint.vue +++ b/packages/modules/web_themes/colors/source/src/components/chargePointList/CPChargePoint.vue @@ -215,18 +215,15 @@
+ - + {{ currentPrice }} ct - + {{ props.chargepoint.etActive @@ -235,19 +232,13 @@ ).toFixed(1) + ' ct' : '-' }} + - - {{ currentPrice }} ct -
- v.visible).length > 1" title="Fahrzeug wechseln" icon="fa-car" :infotext="infotext['vehicle']" :fullwidth="true" > - - + - + - + - - - + --> + + diff --git a/packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/CPConfigInstant.vue b/packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/CPConfigInstant.vue index f210a901a2..141aa08ddf 100755 --- a/packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/CPConfigInstant.vue +++ b/packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/CPConfigInstant.vue @@ -13,18 +13,27 @@ unit="A" /> + + +
@@ -60,7 +69,7 @@ diff --git a/packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/CPConfigScheduled.vue b/packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/CPConfigScheduled.vue index b2d08ffbb6..31409b1343 100755 --- a/packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/CPConfigScheduled.vue +++ b/packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/CPConfigScheduled.vue @@ -94,6 +94,8 @@ function cellStyle(key: number) { } .heading { color: var(--color-battery); + font-size: var(--font-settings); + font-weight: bold; } .left { diff --git a/packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/CPConfigTimed.vue b/packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/CPConfigTimed.vue index 293228118d..cc13c2bdf6 100755 --- a/packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/CPConfigTimed.vue +++ b/packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/CPConfigTimed.vue @@ -91,6 +91,8 @@ function cellStyle(key: number) { .heading { color: var(--color-battery); + font-size: var(--font-settings); + font-weight: bold; } .left { diff --git a/packages/modules/web_themes/colors/source/src/components/chargePointList/cpSimpleList/CpsListItem2.vue b/packages/modules/web_themes/colors/source/src/components/chargePointList/cpSimpleList/CpsListItem2.vue index 623dbc65de..c09fefd349 100755 --- a/packages/modules/web_themes/colors/source/src/components/chargePointList/cpSimpleList/CpsListItem2.vue +++ b/packages/modules/web_themes/colors/source/src/components/chargePointList/cpSimpleList/CpsListItem2.vue @@ -170,7 +170,7 @@ const statusColor = computed(() => { const modeStyle = computed(() => { switch (props.chargepoint.chargeMode) { case 'stop': - return { 'background-color': 'var(--color-input)' } + return { 'background-color': 'var(--fg)' } default: return { 'background-color': chargemodes[props.chargepoint.chargeMode].color, diff --git a/packages/modules/web_themes/colors/source/src/components/chargePointList/model.ts b/packages/modules/web_themes/colors/source/src/components/chargePointList/model.ts index ecd9b5a806..1b03fb1491 100755 --- a/packages/modules/web_themes/colors/source/src/components/chargePointList/model.ts +++ b/packages/modules/web_themes/colors/source/src/components/chargePointList/model.ts @@ -1,5 +1,5 @@ import { computed, reactive } from 'vue' -import { updateServer } from '@/assets/js/sendMessages' +import { updateChargeTemplate, updateServer } from '@/assets/js/sendMessages' import { ChargeMode, type PowerItem } from '@/assets/js/types' import { globalConfig } from '@/assets/js/themeConfig' import { masterData } from '@/assets/js/model' @@ -19,7 +19,7 @@ export class ChargePoint { isCharging = false private _isLocked = false private _connectedVehicle = 0 - chargeTemplate = 0 + chargeTemplate: ChargeTemplate | null = null evTemplate = 0 private _chargeMode = ChargeMode.pv_charging private _hasPriority = false @@ -52,11 +52,22 @@ export class ChargePoint { private _instantTargetCurrent = 0 private _instantTargetSoc = 0 private _instantMaxEnergy = 0 + private _instantTargetPhases = 0 private _pvFeedInLimit = false private _pvMinCurrent = 0 private _pvMaxSoc = 0 private _pvMinSoc = 0 private _pvMinSocCurrent = 0 + private _pvMinSocPhases = 1 + private _pvChargeLimitMode = '' + private _pvTargetSoc = 0 + private _pvMaxEnergy = 0 + private _pvTargetPhases = 0 + private _ecoMinCurrent = 0 + private _ecoTargetPhases = 0 + private _ecoChargeLimitMode = '' + private _ecoTargetSoc = 0 + private _ecoMaxEnergy = 0 private _etActive = false private _etMaxPrice = 20 @@ -96,97 +107,128 @@ export class ChargePoint { } } get chargeMode() { - return this._chargeMode + return this.chargeTemplate?.chargemode.selected ?? ChargeMode.stop } set chargeMode(cm: ChargeMode) { - this._chargeMode = cm - updateServer('chargeMode', cm, this.id) + console.log('set mode') + if (this.chargeTemplate) { + console.log('active') + this.chargeTemplate.chargemode.selected = cm + updateChargeTemplate(this.id) + } } - updateChargeMode(cm: ChargeMode) { + /* updateChargeMode(cm: ChargeMode) { this._chargeMode = cm - } + } */ get hasPriority() { - return this._hasPriority + return this.chargeTemplate?.prio ?? false } set hasPriority(prio: boolean) { - this._hasPriority = prio - updateServer('cpPriority', prio, this.id) + if (this.chargeTemplate) { + this.chargeTemplate.prio = prio + updateServer('cpPriority', prio, this.id) + } } - updateCpPriority(prio: boolean) { + /* updateCpPriority(prio: boolean) { this._hasPriority = prio - } + } */ get timedCharging() { - if (chargeTemplates[this.chargeTemplate]) { - return chargeTemplates[this.chargeTemplate].time_charging.active + if (this.chargeTemplate) { + return this.chargeTemplate.time_charging.active } else { return false } } set timedCharging(setting: boolean) { // chargeTemplates[this.chargeTemplate].time_charging.active = false - chargeTemplates[this.chargeTemplate].time_charging.active = setting - updateServer('cpTimedCharging', setting, this.chargeTemplate) + this.chargeTemplate!.time_charging.active = setting + updateServer('cpTimedCharging', setting, this.id) } get instantTargetCurrent() { - return this._instantTargetCurrent + return this.chargeTemplate?.chargemode.instant_charging.current ?? 0 } set instantTargetCurrent(current: number) { - this._instantTargetCurrent = current - updateServer('cpInstantTargetCurrent', current, this.id) + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.instant_charging.current = current + updateChargeTemplate(this.id) + } } - updateInstantTargetCurrent(current: number) { + /* updateInstantTargetCurrent(current: number) { this._instantTargetCurrent = current } + */ get instantChargeLimitMode() { - return this._instantChargeLimitMode + return ( + this.chargeTemplate?.chargemode.instant_charging.limit.selected ?? 'none' + ) } set instantChargeLimitMode(mode: string) { - this._instantChargeLimitMode = mode - updateServer('cpInstantChargeLimitMode', mode, this.id) + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.instant_charging.limit.selected = mode + updateChargeTemplate(this.id) + } } - updateInstantChargeLimitMode(mode: string) { + /* updateInstantChargeLimitMode(mode: string) { this._instantChargeLimitMode = mode - } + } */ get instantTargetSoc() { - return this._instantTargetSoc + return this.chargeTemplate?.chargemode.instant_charging.limit.soc ?? 0 } set instantTargetSoc(soc: number) { - this._instantTargetSoc = soc - updateServer('cpInstantTargetSoc', soc, this.id) + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.instant_charging.limit.soc = soc + updateChargeTemplate(this.id) + } } - updateInstantTargetSoc(soc: number) { + /* updateInstantTargetSoc(soc: number) { this._instantTargetSoc = soc - } + } */ get instantMaxEnergy() { - return this._instantMaxEnergy + return this.chargeTemplate?.chargemode.instant_charging.limit.amount ?? 0 } set instantMaxEnergy(max: number) { - this._instantMaxEnergy = max - updateServer('cpInstantMaxEnergy', max, this.id) + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.instant_charging.limit.amount = max + updateChargeTemplate(this.id) + } } - updateInstantMaxEnergy(max: number) { + /* updateInstantMaxEnergy(max: number) { this._instantMaxEnergy = max + } */ + get instantTargetPhases() { + return this.chargeTemplate?.chargemode.instant_charging.phases_to_use ?? 0 + } + set instantTargetPhases(phases: number) { + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.instant_charging.phases_to_use = phases + updateChargeTemplate(this.id) + } } + get pvFeedInLimit() { - return this._pvFeedInLimit + return this.chargeTemplate?.chargemode.pv_charging.feed_in_limit ?? false } set pvFeedInLimit(setting: boolean) { - this._pvFeedInLimit = setting - updateServer('cpPvFeedInLimit', setting, this.id) + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.pv_charging.feed_in_limit = setting + updateChargeTemplate(this.id) + } } - updatePvFeedInLimit(setting: boolean) { + /* updatePvFeedInLimit(setting: boolean) { this._pvFeedInLimit = setting - } + } */ get pvMinCurrent() { - return this._pvMinCurrent + return this.chargeTemplate?.chargemode.pv_charging.min_current ?? 0 } set pvMinCurrent(min: number) { - this._pvMinCurrent = min - updateServer('cpPvMinCurrent', min, this.id) + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.pv_charging.min_current = min + updateChargeTemplate(this.id) + } } - updatePvMinCurrent(min: number) { + /* updatePvMinCurrent(min: number) { this._pvMinCurrent = min - } + } */ get pvMaxSoc() { return this._pvMaxSoc } @@ -198,24 +240,135 @@ export class ChargePoint { this._pvMaxSoc = max } get pvMinSoc() { - return this._pvMinSoc + return this.chargeTemplate?.chargemode.pv_charging.min_soc ?? 0 } set pvMinSoc(min: number) { - this._pvMinSoc = min - updateServer('cpPvMinSoc', min, this.id) + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.pv_charging.min_soc = min + updateChargeTemplate(this.id) + } } - updatePvMinSoc(min: number) { + /* updatePvMinSoc(min: number) { this._pvMinSoc = min - } + } */ get pvMinSocCurrent() { - return this._pvMinSocCurrent + return this.chargeTemplate?.chargemode.pv_charging.min_soc_current ?? 0 } set pvMinSocCurrent(a: number) { - this._pvMinSocCurrent = a - updateServer('cpPvMinSocCurrent', a, this.id) + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.pv_charging.min_soc_current = a + updateChargeTemplate(this.id) + } + } + set pvMinSocPhases(n: number) { + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.pv_charging.phases_to_use_min_soc = n + updateChargeTemplate(this.id) + } + } + get pvMinSocPhases() { + return ( + this.chargeTemplate?.chargemode.pv_charging.phases_to_use_min_soc ?? 0 + ) + } + get pvChargeLimitMode() { + return this.chargeTemplate?.chargemode.pv_charging.limit.selected ?? 'none' + } + set pvChargeLimitMode(mode: string) { + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.pv_charging.limit.selected = mode + updateChargeTemplate(this.id) + } + } + get pvTargetSoc() { + return this.chargeTemplate?.chargemode.pv_charging.limit.soc ?? 0 + } + set pvTargetSoc(soc: number) { + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.pv_charging.limit.soc = soc + updateChargeTemplate(this.id) + } + } + get pvMaxEnergy() { + return this.chargeTemplate?.chargemode.pv_charging.limit.amount ?? 0 + } + set pvMaxEnergy(max: number) { + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.pv_charging.limit.amount = max + updateChargeTemplate(this.id) + } + } + get pvTargetPhases() { + return this.chargeTemplate?.chargemode.pv_charging.phases_to_use ?? 0 + } + set pvTargetPhases(phases: number) { + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.pv_charging.phases_to_use = phases + updateChargeTemplate(this.id) + } + } + get ecoMinCurrent() { + return this.chargeTemplate?.chargemode.eco_charging.current ?? 0 + } + set ecoMinCurrent(min: number) { + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.eco_charging.current = min + updateChargeTemplate(this.id) + } + } + get ecoTargetPhases() { + return this.chargeTemplate?.chargemode.eco_charging.phases_to_use ?? 0 + } + set ecoTargetPhases(phases: number) { + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.eco_charging.phases_to_use = phases + updateChargeTemplate(this.id) + } + } + get ecoChargeLimitMode() { + return this.chargeTemplate?.chargemode.eco_charging.limit.selected ?? 'none' + } + set ecoChargeLimitMode(mode: string) { + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.eco_charging.limit.selected = mode + updateChargeTemplate(this.id) + } + } + get ecoTargetSoc() { + return this.chargeTemplate?.chargemode.eco_charging.limit.soc ?? 0 + } + set ecoTargetSoc(soc: number) { + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.eco_charging.limit.soc = soc + updateChargeTemplate(this.id) + } + } + get ecoMaxEnergy() { + return this.chargeTemplate?.chargemode.eco_charging.limit.amount ?? 0 + } + set ecoMaxEnergy(max: number) { + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.eco_charging.limit.amount = max + updateChargeTemplate(this.id) + } + } + get etMaxPrice() { + return ( + (this.chargeTemplate?.chargemode.eco_charging.max_price ?? 0) * 100000 + ) + } + set etMaxPrice(newPrice: number) { + if (this.chargeTemplate) { + this.chargeTemplate.chargemode.eco_charging.max_price = + Math.ceil(newPrice * 1000) / 100000000 + updateChargeTemplate(this.id) + } } - updatePvMinSocCurrent(a: number) { - this._pvMinSocCurrent = a + get etActive() { + return ( + this.chargeTemplate && + this.chargeTemplate.chargemode.selected == ChargeMode.eco_charging + ) } get realCurrent() { switch (this.phasesInUse) { @@ -231,29 +384,6 @@ export class ChargePoint { return 0 } } - get etActive() { - if (vehicles[this.connectedVehicle]) { - return vehicles[this.connectedVehicle].etActive - } else { - return false - } - } - set etActive(val) { - if (vehicles[this.connectedVehicle]) { - vehicles[this.connectedVehicle].etActive = val - } - } - get etMaxPrice() { - return vehicles[this.connectedVehicle].etMaxPrice ?? 0 - } - set etMaxPrice(newPrice: number) { - updateServer( - 'cpEtMaxPrice', - Math.ceil(newPrice * 1000) / 100000000, - this.id, - ) - //updateServer('cpEtMaxPrice', newPrice / 100000, this.id) - } toPowerItem(): PowerItem { return { name: this.name, @@ -279,8 +409,6 @@ export class Vehicle { this.id = index } private _chargeTemplateId = 0 - isSocConfigured = false - isSocManual = false get chargeTemplateId() { return this._chargeTemplateId } @@ -302,25 +430,6 @@ export class Vehicle { updateEvTemplateId(id: number) { this._evTemplateId = id } - get etActive() { - if (chargeTemplates[this.chargeTemplateId]) { - return chargeTemplates[this.chargeTemplateId].et.active - } else { - return false - } - } - set etActive(val) { - if (chargeTemplates[this.chargeTemplateId]) { - updateServer('priceCharging', val, this.chargeTemplateId) - } - } - get etMaxPrice() { - if (chargeTemplates[this.chargeTemplateId]) { - if (chargeTemplates[this.chargeTemplateId].et.active) { - return chargeTemplates[this.chargeTemplateId].et.max_price * 100000 - } - } - } get chargepoint(): ChargePoint | undefined { for (const cp of Object.values(chargePoints)) { if (cp.connectedVehicle == this.id) { @@ -374,34 +483,55 @@ export interface ChargeSchedule { } } export interface ChargeTemplate { + id: number name: string prio: boolean + load_default: boolean + time_charging: { + active: boolean + plans: object + } chargemode: { selected: ChargeMode - instant_charging: { + eco_charging: { current: number + dc_current: number limit: { selected: string soc: number amount: number } + max_price: number + phases_to_use: number } pv_charging: { + dc_min_current: number + dc_min_soc_current: number feed_in_limit: boolean + limit: { + selected: string + amount: number + soc: number + } min_current: number - max_soc: number - min_soc: number min_soc_current: number + min_soc: number + phases_to_use: number + phases_to_use_min_soc: number + } + scheduled_charging: { + plans: object + } + instant_charging: { + current: number + dc_current: number + limit: { + selected: string + soc: number + amount: number + } + phases_to_use: number } - } - time_charging: { - active: boolean - } - disable_after_unplug: boolean - load_default: boolean - et: { - active: boolean - max_price: number } } export interface EvTemplate { @@ -494,3 +624,8 @@ export const topVehicles = computed(() => { } return result }) +export const chargeLimitModes = [ + { name: 'keine', id: 'none' }, + { name: 'Ladestand', id: 'soc' }, + { name: 'Energie', id: 'amount' }, +] diff --git a/packages/modules/web_themes/colors/source/src/components/chargePointList/processMessages.ts b/packages/modules/web_themes/colors/source/src/components/chargePointList/processMessages.ts index fd35958b08..9f025ca99c 100755 --- a/packages/modules/web_themes/colors/source/src/components/chargePointList/processMessages.ts +++ b/packages/modules/web_themes/colors/source/src/components/chargePointList/processMessages.ts @@ -2,7 +2,7 @@ import { usageSummary, globalData, masterData } from '@/assets/js/model' import { chargePoints, vehicles, - chargeTemplates, + //chargeTemplates, evTemplates, Vehicle, type ChargeTimePlan, @@ -11,11 +11,11 @@ import { } from './model' import type { ConnectedVehicleConfig, - ChargeTemplate, + //ChargeTemplate, EvTemplate, ChargeSchedule, } from './model' -import { ChargeMode } from '@/assets/js/types' +// import { ChargeMode } from '@/assets/js/types' export function processChargepointMessages(topic: string, message: string) { const index = getIndex(topic) @@ -110,25 +110,11 @@ export function processChargepointMessages(topic: string, message: string) { ) ) { const config: ConnectedVehicleConfig = JSON.parse(message) - switch (config.chargemode) { - case 'instant_charging': - chargePoints[index].updateChargeMode(ChargeMode.instant_charging) - break - case 'pv_charging': - chargePoints[index].updateChargeMode(ChargeMode.pv_charging) - break - case 'scheduled_charging': - chargePoints[index].updateChargeMode(ChargeMode.scheduled_charging) - break - case 'standby': - chargePoints[index].updateChargeMode(ChargeMode.standby) - break - case 'stop': - chargePoints[index].updateChargeMode(ChargeMode.stop) - break - } - chargePoints[index].chargeTemplate = config.charge_template chargePoints[index].averageConsumption = config.average_consumption + } else if ( + topic.match(/^openwb\/chargepoint\/[0-9]+\/set\/charge_template$/i) + ) { + chargePoints[index].chargeTemplate = JSON.parse(message) } else { // console.warn('Ignored chargepoint message: ' + topic) } @@ -173,15 +159,13 @@ export function processVehicleMessages(topic: string, message: string) { cp.isSocManual = config.type == 'manual' } }) - vehicles[index].isSocConfigured = config.type !== null - vehicles[index].isSocManual = config.type == 'manual' } else { // console.warn('Ignored vehicle message [' + topic + ']=' + message) } } } export function processVehicleTemplateMessages(topic: string, message: string) { - if (topic.match(/^openwb\/vehicle\/template\/charge_template\/[0-9]+$/i)) { + /* if (topic.match(/^openwb\/vehicle\/template\/charge_template\/[0-9]+$/i)) { const match = topic.match(/[0-9]+$/i) if (match) { const index = +match[0] @@ -189,7 +173,8 @@ export function processVehicleTemplateMessages(topic: string, message: string) { chargeTemplates[index] = template updateCpFromChargeTemplate(index, template) } - } else if ( + } else */ + if ( topic.match( /^openwb\/vehicle\/template\/charge_template\/[0-9]+\/time_charging\/plans\/[0-9]+$/i, ) @@ -233,29 +218,29 @@ export function processVehicleTemplateMessages(topic: string, message: string) { // console.warn('Ignored VEHICLE TEMPLATE message [' + topic + ']=' + message) } } -function updateCpFromChargeTemplate(index: number, template: ChargeTemplate) { - Object.values(chargePoints).forEach((cp) => { - if (cp.chargeTemplate == index) { - cp.updateCpPriority(template.prio) - // cp.updateChargeMode(template.chargemode.selected) - cp.updateInstantChargeLimitMode( - template.chargemode.instant_charging.limit.selected, - ) - cp.updateInstantTargetCurrent( - template.chargemode.instant_charging.current, - ) - cp.updateInstantTargetSoc(template.chargemode.instant_charging.limit.soc) - cp.updateInstantMaxEnergy( - template.chargemode.instant_charging.limit.amount, - ) - cp.updatePvFeedInLimit(template.chargemode.pv_charging.feed_in_limit) - cp.updatePvMinCurrent(template.chargemode.pv_charging.min_current) - cp.updatePvMaxSoc(template.chargemode.pv_charging.max_soc) - cp.updatePvMinSoc(template.chargemode.pv_charging.min_soc) - cp.updatePvMinSocCurrent(template.chargemode.pv_charging.min_soc_current) - } - }) -} +//function updateCpFromChargeTemplate(index: number, template: ChargeTemplate) { +// Object.values(chargePoints).forEach((cp) => { +// if (cp.chargeTemplate == index) { +// cp.updateCpPriority(template.prio) +// cp.updateChargeMode(template.chargemode.selected) +//cp.updateInstantChargeLimitMode( +// template.chargemode.instant_charging.limit.selected, +//) +//cp.updateInstantTargetCurrent( +// template.chargemode.instant_charging.current, +//) +//cp.updateInstantTargetSoc(template.chargemode.instant_charging.limit.soc) +//cp.updateInstantMaxEnergy( +// template.chargemode.instant_charging.limit.amount, +//) +//cp.updatePvFeedInLimit(template.chargemode.pv_charging.feed_in_limit) +//cp.updatePvMinCurrent(template.chargemode.pv_charging.min_current) +//cp.updatePvMaxSoc(template.chargemode.pv_charging.max_soc) +//cp.updatePvMinSoc(template.chargemode.pv_charging.min_soc) +//cp.updatePvMinSocCurrent(template.chargemode.pv_charging.min_soc_current) +// } +// }) +//} function getIndex(topic: string): number | undefined { let index = 0 diff --git a/packages/modules/web_themes/colors/source/src/components/counterList/CounterList.vue b/packages/modules/web_themes/colors/source/src/components/counterList/CounterList.vue index a2734fa871..eb11aaca85 100755 --- a/packages/modules/web_themes/colors/source/src/components/counterList/CounterList.vue +++ b/packages/modules/web_themes/colors/source/src/components/counterList/CounterList.vue @@ -6,7 +6,11 @@ > Zähler -
+
diff --git a/packages/modules/web_themes/colors/source/src/components/inverterList/InverterList.vue b/packages/modules/web_themes/colors/source/src/components/inverterList/InverterList.vue index 789d4a2ba7..6055085647 100755 --- a/packages/modules/web_themes/colors/source/src/components/inverterList/InverterList.vue +++ b/packages/modules/web_themes/colors/source/src/components/inverterList/InverterList.vue @@ -11,7 +11,11 @@ {{ formatWatt(sourceSummary.pv.power) }} -
+
diff --git a/packages/modules/web_themes/colors/source/src/components/powerGraph/PgSoc.vue b/packages/modules/web_themes/colors/source/src/components/powerGraph/PgSoc.vue index 7385b562a5..56d5df951c 100755 --- a/packages/modules/web_themes/colors/source/src/components/powerGraph/PgSoc.vue +++ b/packages/modules/web_themes/colors/source/src/components/powerGraph/PgSoc.vue @@ -1,34 +1,32 @@ @@ -77,6 +75,7 @@ const myline = computed(() => { : d['soc' + topVehicles.value[1]!], ) ?? yScale.value(0), ) + let p = path(graphData.data) return p ? p : '' }) @@ -117,11 +116,11 @@ const cpColor = computed(() => { const nameX = computed(() => { switch (props.order) { case 0: - return 3 // first vehicle + return 3 case 1: - return props.width - 3 // 2nd vehicle + return props.width - 3 case 2: - return props.width / 2 // battery + return props.width / 2 default: return 0 // error } @@ -131,16 +130,15 @@ const nameY = computed(() => { if (graphData.data.length > 0) { let index: number switch (props.order) { - case 0: // 1st vehicle - index = 0 + case 0: + index = graphData.data.length - 1 return yScale.value( graphData.data[index]['soc' + topVehicles.value[0]] + 2, ) case 1: - index = graphData.data.length - 1 - return Math.max( - 12, - yScale.value(graphData.data[index]['soc' + topVehicles.value[1]] + 2), + index = 0 + return yScale.value( + graphData.data[index]['soc' + topVehicles.value[1]] + 2, ) case 2: index = Math.round(graphData.data.length / 2) diff --git a/packages/modules/web_themes/colors/source/src/components/powerGraph/PgSourceGraph.vue b/packages/modules/web_themes/colors/source/src/components/powerGraph/PgSourceGraph.vue index 9630a561e2..a64be7c66d 100755 --- a/packages/modules/web_themes/colors/source/src/components/powerGraph/PgSourceGraph.vue +++ b/packages/modules/web_themes/colors/source/src/components/powerGraph/PgSourceGraph.vue @@ -103,17 +103,14 @@ const keysToUse = computed(() => { if (globalConfig.showInverters) { const pattern = /pv\d+/ if (graphData.data.length > 0) { - /* additionalKeys = Object.keys(graphData.data[0]).reduce( - (list: string[], itemKey: string) => { - if (itemKey.match(pattern)) { - list.push(itemKey) + additionalKeys = Object.keys(graphData.data[0]).reduce( + (list: string[], element: string) => { + if (element.match(pattern)) { + list.push(element) } return list }, [], - ) */ - additionalKeys = Object.keys(graphData.data[0]).filter((itemKey) => - itemKey.match(pattern), ) } } diff --git a/packages/modules/web_themes/colors/source/src/components/powerGraph/PgUsageGraph.vue b/packages/modules/web_themes/colors/source/src/components/powerGraph/PgUsageGraph.vue index 23f23d5f1a..7c16ad8c83 100755 --- a/packages/modules/web_themes/colors/source/src/components/powerGraph/PgUsageGraph.vue +++ b/packages/modules/web_themes/colors/source/src/components/powerGraph/PgUsageGraph.vue @@ -141,18 +141,23 @@ const keysToUse = computed(() => { const pattern = /cp\d+/ let additionalKeys: string[] = [] if (graphData.data.length > 0) { - additionalKeys = Object.keys(graphData.data[0]).filter((itemKey) => - itemKey.match(pattern), + additionalKeys = Object.keys(graphData.data[0]).reduce( + (list: string[], element: string) => { + if (element.match(pattern)) { + list.push(element) + } + return list + }, + [], ) } additionalKeys.forEach((key, i) => { k.splice(idx + i, 0, key) - colors[key] = - chargePoints[+key.slice(2)]?.color ?? 'var(--color-charging)' + colors[key] = chargePoints[+key.slice(2)]?.color ?? 'black' }) - /* if (globalConfig.showInverters) { + if (globalConfig.showInverters) { k.push('evuOut') - } */ + } return k } }) diff --git a/packages/modules/web_themes/colors/source/src/components/powerGraph/PgXAxis.vue b/packages/modules/web_themes/colors/source/src/components/powerGraph/PgXAxis.vue index 61b9ac31e7..9d2b9e80b7 100755 --- a/packages/modules/web_themes/colors/source/src/components/powerGraph/PgXAxis.vue +++ b/packages/modules/web_themes/colors/source/src/components/powerGraph/PgXAxis.vue @@ -5,7 +5,7 @@ :transform="'translate(' + 0 + ',' + (height / 2 + 9) + ')'" > - + { return 'zoomed' }) - -const axisWidth = computed(() => { - if (['live', 'today', 'day'].includes(graphData.graphMode)) { - return props.width - } else { - return props.width + props.margin.left + props.margin.right - } -}) diff --git a/packages/modules/web_themes/colors/source/src/components/powerGraph/PowerGraph.vue b/packages/modules/web_themes/colors/source/src/components/powerGraph/PowerGraph.vue index 1d95fa5884..298be554fd 100755 --- a/packages/modules/web_themes/colors/source/src/components/powerGraph/PowerGraph.vue +++ b/packages/modules/web_themes/colors/source/src/components/powerGraph/PowerGraph.vue @@ -49,7 +49,7 @@ :stack-order="globalConfig.usageStackOrder" /> diff --git a/packages/modules/web_themes/colors/source/src/components/powerGraph/SocBat.vue b/packages/modules/web_themes/colors/source/src/components/powerGraph/SocBat.vue deleted file mode 100644 index 7385b562a5..0000000000 --- a/packages/modules/web_themes/colors/source/src/components/powerGraph/SocBat.vue +++ /dev/null @@ -1,193 +0,0 @@ - - - - diff --git a/packages/modules/web_themes/colors/source/src/components/powerGraph/SocCar1.vue b/packages/modules/web_themes/colors/source/src/components/powerGraph/SocCar1.vue deleted file mode 100644 index 7385b562a5..0000000000 --- a/packages/modules/web_themes/colors/source/src/components/powerGraph/SocCar1.vue +++ /dev/null @@ -1,193 +0,0 @@ - - - - diff --git a/packages/modules/web_themes/colors/source/src/components/powerGraph/SocCar2.vue b/packages/modules/web_themes/colors/source/src/components/powerGraph/SocCar2.vue deleted file mode 100644 index 7385b562a5..0000000000 --- a/packages/modules/web_themes/colors/source/src/components/powerGraph/SocCar2.vue +++ /dev/null @@ -1,193 +0,0 @@ - - - - diff --git a/packages/modules/web_themes/colors/source/src/components/powerGraph/SocGeneric.vue b/packages/modules/web_themes/colors/source/src/components/powerGraph/SocGeneric.vue deleted file mode 100644 index 3aa1bb1d41..0000000000 --- a/packages/modules/web_themes/colors/source/src/components/powerGraph/SocGeneric.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - - diff --git a/packages/modules/web_themes/colors/source/src/components/powerGraph/model.ts b/packages/modules/web_themes/colors/source/src/components/powerGraph/model.ts index 005457afa3..9336d6835c 100755 --- a/packages/modules/web_themes/colors/source/src/components/powerGraph/model.ts +++ b/packages/modules/web_themes/colors/source/src/components/powerGraph/model.ts @@ -172,7 +172,7 @@ export const dayGraph = reactive({ graphData.waitForData = true sendCommand({ command: 'getDailyLog', - data: { date: dateString, day: dateString }, + data: { day: dateString }, }) } }, @@ -208,7 +208,7 @@ export const monthGraph = reactive({ graphData.waitForData = true sendCommand({ command: 'getMonthlyLog', - data: { date: dateString, month: dateString }, + data: { month: dateString }, }) }, deactivate() { @@ -256,7 +256,7 @@ export const yearGraph = reactive({ graphData.waitForData = true sendCommand({ command: 'getYearlyLog', - data: { date: dateString, year: dateString }, + data: { year: dateString }, }) }, deactivate() { @@ -360,9 +360,7 @@ export function updateEnergyValues( historicSummary.items.evuOut.energy += values.energy_exported } }) - if (Object.entries(totals.pv).length > 0) { - historicSummary.items.pv.energy = totals.pv.all.energy_exported - } + historicSummary.items.pv.energy = totals.pv.all.energy_exported if (totals.bat.all) { historicSummary.items.batIn.energy = totals.bat.all.energy_imported historicSummary.items.batOut.energy = totals.bat.all.energy_exported diff --git a/packages/modules/web_themes/colors/source/src/components/powerGraph/processMonthYearGraphData.ts b/packages/modules/web_themes/colors/source/src/components/powerGraph/processMonthYearGraphData.ts index 3b35a39f7f..25eb59fc1f 100755 --- a/packages/modules/web_themes/colors/source/src/components/powerGraph/processMonthYearGraphData.ts +++ b/packages/modules/web_themes/colors/source/src/components/powerGraph/processMonthYearGraphData.ts @@ -56,7 +56,6 @@ export function processMonthGraphMessages(topic: string, message: string) { // reloadMonthGraph(topic, message) } export function processYearGraphMessages(topic: string, message: string) { - console.log("process data") //const inputTable: RawDayGraphDataItem[] = JSON.parse(message).entries //const energyValues: RawDayGraphDataItem = JSON.parse(message).totals const { diff --git a/packages/modules/web_themes/colors/source/src/components/priceChart/PriceChart.vue b/packages/modules/web_themes/colors/source/src/components/priceChart/PriceChart.vue index 9542cfcbef..e21ae6fc78 100755 --- a/packages/modules/web_themes/colors/source/src/components/priceChart/PriceChart.vue +++ b/packages/modules/web_themes/colors/source/src/components/priceChart/PriceChart.vue @@ -1,5 +1,4 @@

Preisbasiertes Laden:

Anbieter: {{ etData.etProvider }}


@@ -15,7 +14,6 @@