From 14cfd696cb55fed1c1276171a0031ae48e32e75b Mon Sep 17 00:00:00 2001 From: LKuemmel <76958050+LKuemmel@users.noreply.github.com> Date: Tue, 3 Sep 2024 12:38:50 +0200 Subject: [PATCH] Feature dc adapter (#1864) * DC charging * fix test * fix max current * fix rebase * DC charging * draft * draft * fixes * naming * update config * fix test * fix test * undo * fix update config * fix * remove old code * fix configuration.json --- packages/conftest.py | 8 +- .../control/algorithm/additional_current.py | 11 +- packages/control/algorithm/common.py | 48 ++++--- packages/control/algorithm/common_test.py | 32 ++--- .../algorithm/integration_test/conftest.py | 3 +- .../integration_test/instant_charging_test.py | 4 + .../integration_test/pv_charging_test.py | 4 + packages/control/algorithm/min_current.py | 26 ++-- .../control/algorithm/surplus_controlled.py | 4 +- packages/control/chargepoint/chargepoint.py | 42 ++++-- .../control/chargepoint/chargepoint_data.py | 3 + .../chargepoint/chargepoint_template.py | 3 + packages/control/chargepoint/charging_type.py | 6 + .../control/chargepoint/control_parameter.py | 3 + packages/control/counter.py | 2 +- packages/control/counter_all.py | 4 +- packages/control/ev.py | 132 ++++++++++++------ packages/control/ev_charge_template_test.py | 14 +- packages/control/optional.py | 4 + packages/helpermodules/abstract_plans.py | 2 + .../changed_values_handler_test.py | 19 ++- .../helpermodules/hardware_configuration.py | 25 ++-- packages/helpermodules/setdata.py | 31 +++- packages/helpermodules/subdata.py | 19 ++- packages/helpermodules/update_config.py | 31 +++- .../chargepoints/external_openwb/config.py | 9 +- .../chargepoints/internal_openwb/config.py | 9 +- packages/modules/chargepoints/mqtt/config.py | 10 +- .../openwb_dc_adapter/__init__.py | 0 .../openwb_dc_adapter/chargepoint_module.py | 125 +++++++++++++++++ .../chargepoints/openwb_dc_adapter/config.py | 20 +++ .../modules/chargepoints/openwb_pro/config.py | 9 +- .../openwb_series2_satellit/config.py | 9 +- .../modules/chargepoints/smartwb/config.py | 9 +- .../modules/common/abstract_chargepoint.py | 30 ++++ packages/modules/common/component_state.py | 6 + packages/modules/common/simcount/__init__.py | 2 +- .../modules/common/simcount/_simcounter.py | 10 ++ packages/modules/common/store/_chargepoint.py | 5 + packages/modules/configuration.py | 9 +- .../update_values_test.py | 2 +- packages/modules/update_soc.py | 4 + .../web_themes/standard_legacy/web/index.html | 69 +++++++++ .../standard_legacy/web/processAllMqttMsg.js | 27 ++++ .../standard_legacy/web/setupMqttServices.js | 1 + 45 files changed, 660 insertions(+), 185 deletions(-) create mode 100644 packages/control/chargepoint/charging_type.py create mode 100644 packages/modules/chargepoints/openwb_dc_adapter/__init__.py create mode 100644 packages/modules/chargepoints/openwb_dc_adapter/chargepoint_module.py create mode 100644 packages/modules/chargepoints/openwb_dc_adapter/config.py diff --git a/packages/conftest.py b/packages/conftest.py index 11bb3faba2..f757e29da9 100644 --- a/packages/conftest.py +++ b/packages/conftest.py @@ -14,7 +14,13 @@ from control.counter_all import CounterAll from control.pv import Pv, PvData from control.pv import Get as PvGet -from helpermodules import pub, timecheck +from helpermodules import hardware_configuration, pub, timecheck + + +@pytest.fixture(autouse=True) +def mock_open_file(monkeypatch) -> None: + mock_config = Mock(return_value={"dc_charging": False, "openwb-version": 1, "max_c_socket": 32}) + monkeypatch.setattr(hardware_configuration, "_read_configuration", mock_config) @pytest.fixture(autouse=True) diff --git a/packages/control/algorithm/additional_current.py b/packages/control/algorithm/additional_current.py index 4b5e6ddc1e..a57daa48a0 100644 --- a/packages/control/algorithm/additional_current.py +++ b/packages/control/algorithm/additional_current.py @@ -29,13 +29,15 @@ def set_additional_current(self) -> None: cp = preferenced_chargepoints[0] missing_currents, counts = common.get_missing_currents_left(preferenced_chargepoints) available_currents, limit = Loadmanagement().get_available_currents(missing_currents, counter) + log.debug(f"cp {cp.num} available currents {available_currents} missing currents " + f"{missing_currents} limit {limit}") cp.data.control_parameter.limit = limit available_for_cp = common.available_current_for_cp(cp, counts, available_currents, missing_currents) current = common.get_current_to_set( cp.data.set.current, available_for_cp, cp.data.set.target_current) self._set_loadmangement_message(current, limit, cp, counter) common.set_current_counterdiff( - current - cp.data.set.charging_ev_data.ev_template.data.min_current, + cp.data.control_parameter.min_current, current, cp) preferenced_chargepoints.pop(0) @@ -50,9 +52,12 @@ def _set_loadmangement_message(self, chargepoint: Chargepoint, counter: Counter) -> None: # Strom muss an diesem Zähler geändert werden + log.debug( + f"current {current} target {chargepoint.data.set.target_current} set current {chargepoint.data.set.current}" + f" required currents {chargepoint.data.control_parameter.required_currents}") if (current != max(chargepoint.data.set.target_current, chargepoint.data.set.current or 0) and # Strom erreicht nicht die vorgegebene Stromstärke - current != max( - chargepoint.data.control_parameter.required_currents)): + round(current, 2) != round(max( + chargepoint.data.control_parameter.required_currents), 2)): chargepoint.set_state_and_log(f"Es kann nicht mit der vorgegebenen Stromstärke geladen werden" f"{limit.value.format(get_component_name_by_id(counter.num))}") diff --git a/packages/control/algorithm/common.py b/packages/control/algorithm/common.py index 9df5b2b9c6..72556dcc73 100644 --- a/packages/control/algorithm/common.py +++ b/packages/control/algorithm/common.py @@ -6,6 +6,7 @@ from control.chargemode import Chargemode from control.chargepoint.chargepoint import Chargepoint from control.counter import Counter +from helpermodules.timecheck import check_timestamp from modules.common.component_type import ComponentType log = logging.getLogger(__name__) @@ -29,6 +30,8 @@ (None, Chargemode.STOP, True), (None, Chargemode.STOP, False)) +LESS_CHARGING_TIMEOUT = 60 + # tested @@ -62,12 +65,11 @@ def mode_and_counter_generator(chargemodes: List) -> Iterable[Tuple[Tuple[Option def get_min_current(chargepoint: Chargepoint) -> Tuple[List[float], List[int]]: min_currents = [0.0]*3 counts = [0]*3 - charging_ev_data = chargepoint.data.set.charging_ev_data required_currents = chargepoint.data.control_parameter.required_currents for i in range(3): if required_currents[i] != 0: counts[i] += 1 - min_currents[i] = charging_ev_data.ev_template.data.min_current + min_currents[i] = chargepoint.data.control_parameter.min_current else: min_currents[i] = 0 return min_currents, counts @@ -75,8 +77,15 @@ def get_min_current(chargepoint: Chargepoint) -> Tuple[List[float], List[int]]: # tested -def set_current_counterdiff(diff: float, current: float, chargepoint: Chargepoint, surplus: bool = False) -> None: +def set_current_counterdiff(diff_curent: float, + current: float, + chargepoint: Chargepoint, + surplus: bool = False) -> None: required_currents = chargepoint.data.control_parameter.required_currents + considered_current = consider_less_charging_chargepoint_in_loadmanagement( + chargepoint, current) + # gar nicht ladende Autos? + diff = max(considered_current - diff_curent, 0) diffs = [diff if required_currents[i] != 0 else 0 for i in range(3)] if max(diffs) > 0: counters = data.data.counter_all_data.get_counters_to_check(chargepoint.num) @@ -129,21 +138,20 @@ def update_raw_data(preferenced_chargepoints: List[Chargepoint], """alle CP, die schon einen Sollstrom haben, wieder rausrechnen, da dieser neu gesetzt wird und die neue Differenz bei den Zählern eingetragen wird.""" for chargepoint in preferenced_chargepoints: - if consider_not_charging_chargepoint_in_loadmanagement(chargepoint): - continue - charging_ev_data = chargepoint.data.set.charging_ev_data required_currents = chargepoint.data.control_parameter.required_currents max_target_set_current = max(chargepoint.data.set.target_current, chargepoint.data.set.current or 0) + max_target_set_current = consider_less_charging_chargepoint_in_loadmanagement( + chargepoint, max_target_set_current) if diff_to_zero is False: - if charging_ev_data.ev_template.data.min_current < max_target_set_current: - diffs = [charging_ev_data.ev_template.data.min_current - + if chargepoint.data.control_parameter.min_current < max_target_set_current: + diffs = [chargepoint.data.control_parameter.min_current - max_target_set_current if required_currents[i] != 0 else 0 for i in range(3)] else: continue else: - if charging_ev_data.ev_template.data.min_current <= max_target_set_current: - diffs = [-charging_ev_data.ev_template.data.min_current if required_currents[i] + if chargepoint.data.control_parameter.min_current <= max_target_set_current: + diffs = [-chargepoint.data.control_parameter.min_current if required_currents[i] != 0 else 0 for i in range(3)] else: continue @@ -155,10 +163,17 @@ def update_raw_data(preferenced_chargepoints: List[Chargepoint], data.data.counter_data[counter].update_values_left(diffs) -def consider_not_charging_chargepoint_in_loadmanagement(cp: Chargepoint) -> bool: - # tested - return data.data.counter_all_data.data.config.reserve_for_not_charging is False and max(cp.data.get.currents) == 0 - +def consider_less_charging_chargepoint_in_loadmanagement(cp: Chargepoint, set_current: float) -> bool: + if (data.data.counter_all_data.data.config.consider_less_charging and + ((set_current - + cp.data.set.charging_ev_data.ev_template.data.nominal_difference) > max(cp.data.get.currents) and + cp.data.control_parameter.timestamp_charge_start is not None and + check_timestamp(cp.data.control_parameter.timestamp_charge_start, LESS_CHARGING_TIMEOUT) is False)): + log.debug( + f"LP {cp.num} lädt deutlich unter dem Sollstrom und wird nur mit {cp.data.get.currents}A berücksichtigt.") + return max(cp.data.get.currents) + else: + return set_current # tested @@ -166,15 +181,14 @@ def get_missing_currents_left(preferenced_chargepoints: List[Chargepoint]) -> Tu missing_currents = [0.0]*3 counts = [0]*3 for chargepoint in preferenced_chargepoints: - charging_ev_data = chargepoint.data.set.charging_ev_data required_currents = chargepoint.data.control_parameter.required_currents for i in range(0, 3): if required_currents[i] != 0: counts[i] += 1 try: - missing_currents[i] += required_currents[i] - charging_ev_data.ev_template.data.min_current + missing_currents[i] += required_currents[i] - chargepoint.data.control_parameter.min_current except KeyError: - missing_currents[i] += max(required_currents) - charging_ev_data.ev_template.data.min_current + missing_currents[i] += max(required_currents) - chargepoint.data.control_parameter.min_current else: missing_currents[i] += 0 return missing_currents, counts diff --git a/packages/control/algorithm/common_test.py b/packages/control/algorithm/common_test.py index 6028332a8e..21c23c8b88 100644 --- a/packages/control/algorithm/common_test.py +++ b/packages/control/algorithm/common_test.py @@ -34,10 +34,9 @@ def test_reset_current(set_current: int, expected_current: int): @pytest.mark.parametrize( "diff, required_currents, expected_set_current, expected_diffs", [ - pytest.param(2, [10, 0, 0], 8, [2, 0, 0], id="set diff one phase"), - pytest.param(2, [12]*3, 8, [2]*3, id="set diff three phases"), - pytest.param(8, [8]*3, 8, [8]*3, id="set min current three phases"), - pytest.param(0, [8]*3, 8, [0]*3, id="min current is already set, three phases"), + pytest.param(10, [10, 0, 0], 10, [2, 0, 0], id="set diff one phase"), + pytest.param(10, [12]*3, 10, [2]*3, id="set diff three phases"), + pytest.param(8, [8]*3, 8, [0]*3, id="min current is already set, three phases"), ]) def test_set_current_counterdiff(diff: float, required_currents: List[float], @@ -55,11 +54,11 @@ def test_set_current_counterdiff(diff: float, data.data.counter_data = {"cp0": Mock(spec=Counter), "cp6": Mock(spec=Counter)} # evaluation - common.set_current_counterdiff(diff, 8, cp) + common.set_current_counterdiff(8, diff, cp) # assertion assert cp.data.set.current == expected_set_current - if diff != 0: + if max(expected_diffs) != 0: assert data.data._counter_data['cp0'].update_values_left.call_args_list[0][0][0] == expected_diffs assert data.data._counter_data['cp6'].update_values_left.call_args_list[0][0][0] == expected_diffs @@ -152,23 +151,24 @@ def setup_cp(num: int, required_currents) -> Chargepoint: @pytest.mark.parametrize( - "reserve_for_not_charging, get_currents, expected_considered", + "consider_less_charging, get_currents, expected_considered", [ - pytest.param(True, [0]*3, False, id="reserve_for_not_charging active"), - pytest.param(True, [6]*3, False, id="reserve_for_not_charging active"), - pytest.param(False, [0]*3, True, id="not charging"), - pytest.param(False, [6]*3, False, id="charging"), + pytest.param(True, [6]*3, 6, id="consider_less_charging active, charging less"), + pytest.param(True, [10]*3, 10, id="consider_less_charging active, charging with set current"), + pytest.param(False, [0]*3, 10, id="consider_less_charging inactive"), ]) -def test_consider_not_charging_chargepoint_in_loadmanagement(reserve_for_not_charging: bool, - get_currents: List[float], - expected_considered: bool): +def test_consider_less_charging_chargepoint_in_loadmanagement(consider_less_charging: bool, + get_currents: List[float], + expected_considered: bool): # setup cp = Chargepoint(4, None) cp.data.get.currents = get_currents - data.data.counter_all_data.data.config.reserve_for_not_charging = reserve_for_not_charging + cp.data.set.current = 10 + cp.data.control_parameter.timestamp_charge_start = 1652683152 + data.data.counter_all_data.data.config.consider_less_charging = consider_less_charging # evaluation - considered = common.consider_not_charging_chargepoint_in_loadmanagement(cp) + considered = common.consider_less_charging_chargepoint_in_loadmanagement(cp, 10) # assertion assert considered == expected_considered diff --git a/packages/control/algorithm/integration_test/conftest.py b/packages/control/algorithm/integration_test/conftest.py index ac2f44d16f..9126cbcf59 100644 --- a/packages/control/algorithm/integration_test/conftest.py +++ b/packages/control/algorithm/integration_test/conftest.py @@ -31,6 +31,7 @@ def data_() -> None: data.data.cp_data[f"cp{i}"].data.set.plug_time = f"12/01/2022, 15:0{i}:11" data.data.cp_data[f"cp{i}"].data.set.charging_ev_data.ev_template.data.nominal_difference = 2 data.data.cp_data["cp3"].data.set.charging_ev_data.ev_template.data.min_current = 10 + data.data.cp_data["cp3"].data.control_parameter.min_current = 10 data.data.bat_data.update({"bat2": Bat(2), "all": BatAll()}) data.data.pv_data.update({"pv1": Pv(1)}) data.data.counter_data.update({ @@ -46,7 +47,7 @@ def data_() -> None: data.data.counter_data["counter6"].data.config.max_total_power = 11000 data.data.counter_all_data = CounterAll() data.data.counter_all_data.data.get.hierarchy = NESTED_HIERARCHY - data.data.counter_all_data.data.config.reserve_for_not_charging = True + data.data.counter_all_data.data.config.consider_less_charging = True @dataclass diff --git a/packages/control/algorithm/integration_test/instant_charging_test.py b/packages/control/algorithm/integration_test/instant_charging_test.py index c4bb4dfddc..f0582261d5 100644 --- a/packages/control/algorithm/integration_test/instant_charging_test.py +++ b/packages/control/algorithm/integration_test/instant_charging_test.py @@ -16,6 +16,8 @@ def all_cp_instant_charging_1p(): for i in range(3, 6): control_parameter = data.data.cp_data[f"cp{i}"].data.control_parameter + control_parameter.min_current = data.data.cp_data[ + f"cp{i}"].data.set.charging_ev_data.ev_template.data.min_current control_parameter.required_currents = [0]*3 control_parameter.required_currents[i-3] = 16 control_parameter.required_current = 16 @@ -34,6 +36,8 @@ def all_cp_charging_1p(): def all_cp_instant_charging_3p(): for i in range(3, 6): control_parameter = data.data.cp_data[f"cp{i}"].data.control_parameter + control_parameter.min_current = data.data.cp_data[ + f"cp{i}"].data.set.charging_ev_data.ev_template.data.min_current control_parameter.required_currents = [16]*3 control_parameter.required_current = 16 control_parameter.chargemode = Chargemode.INSTANT_CHARGING diff --git a/packages/control/algorithm/integration_test/pv_charging_test.py b/packages/control/algorithm/integration_test/pv_charging_test.py index eb866ce506..0b9eefb4f1 100644 --- a/packages/control/algorithm/integration_test/pv_charging_test.py +++ b/packages/control/algorithm/integration_test/pv_charging_test.py @@ -22,6 +22,8 @@ def all_cp_pv_charging_3p(): f"cp{i}"].data.set.charging_ev_data.ev_template.data.min_current control_parameter.required_currents = [ data.data.cp_data[f"cp{i}"].data.set.charging_ev_data.ev_template.data.min_current]*3 + control_parameter.min_current = data.data.cp_data[ + f"cp{i}"].data.set.charging_ev_data.ev_template.data.min_current control_parameter.chargemode = Chargemode.PV_CHARGING control_parameter.submode = Chargemode.PV_CHARGING control_parameter.phases = 3 @@ -52,6 +54,8 @@ def all_cp_pv_charging_1p(): for i in range(3, 6): control_parameter = data.data.cp_data[f"cp{i}"].data.control_parameter charging_ev_data = data.data.cp_data[f"cp{i}"].data.set.charging_ev_data + control_parameter.min_current = data.data.cp_data[ + f"cp{i}"].data.set.charging_ev_data.ev_template.data.min_current control_parameter.required_current = data.data.cp_data[ f"cp{i}"].data.set.charging_ev_data.ev_template.data.min_current control_parameter.required_currents = [0]*3 diff --git a/packages/control/algorithm/min_current.py b/packages/control/algorithm/min_current.py index ac5af0f213..2d1cd918b2 100644 --- a/packages/control/algorithm/min_current.py +++ b/packages/control/algorithm/min_current.py @@ -29,24 +29,16 @@ def set_min_current(self) -> None: cp, counts, available_currents, missing_currents) current = common.get_current_to_set( cp.data.set.current, available_for_cp, cp.data.set.target_current) - if common.consider_not_charging_chargepoint_in_loadmanagement(cp): - cp.data.set.current = cp.data.set.charging_ev_data.ev_template.data.min_current - log.debug( - f"LP{cp.num}: Stromstärke {cp.data.set.charging_ev_data.ev_template.data.min_current}" - "A. Zuteilung ohne Berücksichtigung im Lastmanagement, da kein Ladestart zu erwarten " - "ist und Reserve für nicht-ladende inaktiv.") + if current < cp.data.control_parameter.min_current: + common.set_current_counterdiff(-(cp.data.set.current or 0), 0, cp) + if limit: + cp.set_state_and_log( + f"Ladung kann nicht gestartet werden{limit.value.format(counter.num)}") else: - if current < cp.data.set.charging_ev_data.ev_template.data.min_current: - common.set_current_counterdiff(-(cp.data.set.current or 0), 0, cp) - if limit: - cp.set_state_and_log( - f"Ladung kann nicht gestartet werden{limit.value.format(counter.num)}") - else: - common.set_current_counterdiff( - (cp.data.set.charging_ev_data.ev_template.data.min_current - - cp.data.set.target_current), - cp.data.set.charging_ev_data.ev_template.data.min_current, - cp) + common.set_current_counterdiff( + cp.data.set.target_current, + cp.data.control_parameter.min_current, + cp) else: cp.data.set.current = 0 preferenced_chargepoints.pop(0) diff --git a/packages/control/algorithm/surplus_controlled.py b/packages/control/algorithm/surplus_controlled.py index 85c8fcda98..4909177c85 100644 --- a/packages/control/algorithm/surplus_controlled.py +++ b/packages/control/algorithm/surplus_controlled.py @@ -57,7 +57,7 @@ def _set(self, limited_current = self._limit_adjust_current(cp, current) limited_current = self._add_unused_evse_current(limited_current, cp) common.set_current_counterdiff( - limited_current - cp.data.set.charging_ev_data.ev_template.data.min_current, + cp.data.control_parameter.min_current, limited_current, cp, surplus=True) @@ -107,7 +107,7 @@ def _limit_adjust_current(self, chargepoint: Chargepoint, new_current: float) -> current = max(chargepoint.data.get.currents) + MAX_CURRENT msg = "Es darf um max 5A über den aktuell genutzten Strom geregelt werden." chargepoint.set_state_and_log(msg) - return max(current, chargepoint.data.set.charging_ev_data.ev_template.data.min_current) + return max(current, chargepoint.data.control_parameter.min_current) def _add_unused_evse_current(self, limited_current, chargepoint: Chargepoint) -> float: """Wenn Autos nicht die volle Ladeleistung nutzen, wird unnötig eingespeist. Dann kann um den noch nicht diff --git a/packages/control/chargepoint/chargepoint.py b/packages/control/chargepoint/chargepoint.py index 647b6d2c26..ed3f65f7a5 100644 --- a/packages/control/chargepoint/chargepoint.py +++ b/packages/control/chargepoint/chargepoint.py @@ -27,6 +27,7 @@ from control.chargepoint.chargepoint_data import ChargepointData, ConnectedConfig, ConnectedInfo, ConnectedSoc, Get, Log from control.chargepoint.chargepoint_template import CpTemplate 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 import Ev from control import phase_switch @@ -276,6 +277,10 @@ def set_control_parameter(self, submode: str, required_current: float): 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.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 + else: + self.data.control_parameter.min_current = self.data.set.charging_ev_data.ev_template.data.dc_min_current except Exception: log.exception("Fehler im LP-Modul "+str(self.num)) @@ -584,14 +589,19 @@ def set_phases(self, phases: int) -> int: def check_min_max_current(self, required_current: float, phases: int, pv: bool = False) -> float: required_current_prev = required_current - required_current, msg = self.data.set.charging_ev_data.check_min_max_current(self.data.control_parameter, - required_current, - phases, - pv) - if phases == 1: - required_current = min(required_current, self.template.data.max_current_single_phase) + required_current, msg = self.data.set.charging_ev_data.check_min_max_current( + self.data.control_parameter, + required_current, + phases, + self.template.data.charging_type, + pv) + if self.template.data.charging_type == ChargingType.AC.value: + if phases == 1: + required_current = min(required_current, self.template.data.max_current_single_phase) + else: + required_current = min(required_current, self.template.data.max_current_multi_phases) else: - required_current = min(required_current, self.template.data.max_current_multi_phases) + required_current = min(required_current, self.template.data.dc_max_current) if required_current != required_current_prev and msg is None: msg = ("Die Einstellungen in dem Ladepunkt-Profil beschränken den Strom auf " f"maximal {required_current} A.") @@ -611,6 +621,18 @@ def set_required_currents(self, required_current: float) -> None: "was ggf eine unnötige Reduktion der Ladeleistung zur Folge hat.") self.data.set.required_power = sum(control_parameter.required_currents) * 230 + def handle_less_power(self): + if self.data.set.current != 0 and self.data.control_parameter.state == ChargepointState.CHARGING_ALLOWED: + nominal_difference = self.data.set.charging_ev_data.ev_template.data.nominal_difference + if self.data.set.current - nominal_difference > max(self.data.get.currents): + if self.data.control_parameter.timestamp_charge_start is None: + self.data.control_parameter.timestamp_charge_start = create_timestamp() + else: + self.data.control_parameter.timestamp_charge_start = None + else: + # Beim Ladestart Timer laufen lassen, manche Fahrzeuge brauchen sehr lange. + self.data.control_parameter.timestamp_charge_start = None + def update_ev(self, ev_list: Dict[str, Ev]) -> None: # Für Control-Pilot-Unterbrechung set current merken. self.set_current_prev = self.data.set.current @@ -639,16 +661,20 @@ def update(self, ev_list: Dict[str, Ev]) -> None: self.data.control_parameter, self.data.get.imported, max_phase_hw, - self.cp_ev_support_phase_switch()) + self.cp_ev_support_phase_switch(), + self.template.data.charging_type) phases = self.set_phases(phases) self._pub_connected_vehicle(charging_ev) + required_current = self.chargepoint_module.add_conversion_loss_to_current(required_current) # Einhaltung des Minimal- und Maximalstroms prüfen 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_control_parameter(submode, required_current) self.set_required_currents(required_current) + self.handle_less_power() if charging_ev.chargemode_changed: data.data.counter_all_data.get_evu_counter().reset_switch_on_off( diff --git a/packages/control/chargepoint/chargepoint_data.py b/packages/control/chargepoint/chargepoint_data.py index ca4fdf1728..15c9876330 100644 --- a/packages/control/chargepoint/chargepoint_data.py +++ b/packages/control/chargepoint/chargepoint_data.py @@ -90,6 +90,9 @@ def connected_vehicle_factory() -> ConnectedVehicle: @dataclass class Get: charge_state: bool = False + charging_current: Optional[float] = 0 + charging_power: Optional[float] = 0 + charging_voltage: Optional[float] = 0 connected_vehicle: ConnectedVehicle = field(default_factory=connected_vehicle_factory) currents: List[float] = field(default_factory=currents_list_factory) daily_imported: float = 0 diff --git a/packages/control/chargepoint/chargepoint_template.py b/packages/control/chargepoint/chargepoint_template.py index c2591de2a1..dd21bd5fb1 100644 --- a/packages/control/chargepoint/chargepoint_template.py +++ b/packages/control/chargepoint/chargepoint_template.py @@ -5,6 +5,7 @@ from control import data from control import ev as ev_module +from control.chargepoint.charging_type import ChargingType from dataclass_utils.factories import empty_dict_factory, empty_list_factory from helpermodules.abstract_plans import AutolockPlan from helpermodules import timecheck @@ -37,9 +38,11 @@ def autolock_factory(): @dataclass class CpTemplateData: autolock: Autolock = field(default_factory=autolock_factory, metadata={"topic": ""}) + charging_type: str = ChargingType.AC.value id: int = 0 max_current_multi_phases: int = 32 max_current_single_phase: int = 32 + dc_max_current: float = 435 name: str = "neues Ladepunkt-Profil" disable_after_unplug: bool = False valid_tags: List = field(default_factory=empty_list_factory) diff --git a/packages/control/chargepoint/charging_type.py b/packages/control/chargepoint/charging_type.py new file mode 100644 index 0000000000..c49b54cf40 --- /dev/null +++ b/packages/control/chargepoint/charging_type.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class ChargingType(Enum): + DC = "DC" + AC = "AC" diff --git a/packages/control/chargepoint/control_parameter.py b/packages/control/chargepoint/control_parameter.py index de8daa2ed4..c1abeb035d 100644 --- a/packages/control/chargepoint/control_parameter.py +++ b/packages/control/chargepoint/control_parameter.py @@ -18,6 +18,7 @@ class ControlParameter: imported_instant_charging: Optional[float] = field( default=None, metadata={"topic": "control_parameter/imported_instant_charging"}) limit: Optional[LimitingValue] = field(default=None, metadata={"topic": "control_parameter/limit"}) + min_current: int = field(default=6, metadata={"topic": "control_parameter/min_current"}) phases: int = field(default=0, metadata={"topic": "control_parameter/phases"}) prio: bool = field(default=False, metadata={"topic": "control_parameter/prio"}) required_current: float = field(default=0, metadata={"topic": "control_parameter/required_current"}) @@ -27,6 +28,8 @@ class ControlParameter: submode: Chargemode_enum = field(default=Chargemode_enum.STOP, metadata={"topic": "control_parameter/submode"}) timestamp_auto_phase_switch: Optional[float] = field( default=None, metadata={"topic": "control_parameter/timestamp_auto_phase_switch"}) + timestamp_charge_start: Optional[float] = field( + default=None, metadata={"topic": "control_parameter/timestamp_charge_start"}) timestamp_perform_phase_switch: Optional[float] = field( default=None, metadata={"topic": "control_parameter/timestamp_perform_phase_switch"}) timestamp_switch_on_off: Optional[float] = field( diff --git a/packages/control/counter.py b/packages/control/counter.py index 580b052dff..81da8a6dc3 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -420,7 +420,7 @@ def switch_off_check_threshold(self, chargepoint: Chargepoint) -> bool: control_parameter.state = ChargepointState.CHARGING_ALLOWED else: # Wurde die Abschaltschwelle ggf. durch die Verzögerung anderer LP erreicht? - min_current = (charging_ev_data.ev_template.data.min_current + min_current = (chargepoint.data.control_parameter.min_current + charging_ev_data.ev_template.data.nominal_difference) switch_off_condition = (power_in_use > threshold or # Wenn der Speicher hochregeln soll, muss auch abgeschaltet werden. diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index b8f2272527..0f1e919ad2 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -22,8 +22,8 @@ class Config: home_consumption_source_id: Optional[str] = field( default=None, metadata={"topic": "config/home_consumption_source_id"}) - reserve_for_not_charging: bool = field( - default=False, metadata={"topic": "config/reserve_for_not_charging"}) + consider_less_charging: bool = field( + default=False, metadata={"topic": "config/consider_less_charging"}) def config_factory() -> Config: diff --git a/packages/control/ev.py b/packages/control/ev.py index 0c312c6e37..5141e5dfa5 100644 --- a/packages/control/ev.py +++ b/packages/control/ev.py @@ -13,6 +13,7 @@ from control import data 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.limiting_value import LimitingValue from dataclass_utils.factories import empty_dict_factory, empty_list_factory @@ -67,11 +68,14 @@ class TimeCharging: @dataclass class InstantCharging: current: int = 10 + dc_current: float = 145 limit: Limit = field(default_factory=limit_factory) @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 @@ -133,6 +137,8 @@ def charge_template_data_factory() -> ChargeTemplateData: @dataclass class EvTemplateData: + dc_min_current: int = 0 + dc_max_current: int = 0 name: str = "neues Fahrzeug-Profil" max_current_multi_phases: int = 16 max_phases: int = 3 @@ -243,7 +249,8 @@ def get_required_current(self, control_parameter: ControlParameter, imported: float, max_phases_hw: int, - phase_switch_supported: bool) -> Tuple[bool, Optional[str], str, float, int]: + phase_switch_supported: bool, + charging_type: str) -> Tuple[bool, Optional[str], str, float, int]: """ ermittelt, ob und mit welchem Strom das EV geladen werden soll (unabhängig vom Lastmanagement) Parameter @@ -277,7 +284,8 @@ def get_required_current(self, control_parameter.phases, used_amount, max_phases_hw, - phase_switch_supported) + phase_switch_supported, + charging_type) soc_request_intervall_offset = 0 if plan_data: name = self.charge_template.data.chargemode.scheduled_charging.plans[plan_data.num].name @@ -300,7 +308,7 @@ def get_required_current(self, self.data.get.soc, used_amount, control_parameter.phases, - self.ev_template.data.min_current, + control_parameter.min_current, soc_request_intervall_offset) # Wenn Zielladen auf Überschuss wartet, prüfen, ob Zeitladen aktiv ist. @@ -311,7 +319,8 @@ def get_required_current(self, used_amount = imported - control_parameter.imported_at_plan_start tmp_current, tmp_submode, tmp_message, name = self.charge_template.time_charging( self.data.get.soc, - used_amount + used_amount, + charging_type ) # Info vom Zielladen erhalten message = f"{message or ''} {tmp_message or ''}".strip() @@ -331,10 +340,11 @@ def get_required_current(self, used_amount = imported - control_parameter.imported_instant_charging required_current, submode, message = self.charge_template.instant_charging( self.data.get.soc, - used_amount) + used_amount, + charging_type) elif self.charge_template.data.chargemode.selected == "pv_charging": required_current, submode, message = self.charge_template.pv_charging( - self.data.get.soc, self.ev_template.data.min_current) + 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: @@ -369,7 +379,8 @@ def check_min_max_current(self, control_parameter: ControlParameter, required_current: float, phases: int, - pv: bool = False) -> Tuple[float, Optional[str]]: + charging_type: str, + pv: bool = False,) -> Tuple[float, Optional[str]]: """ prüft, ob der gesetzte Ladestrom über dem Mindest-Ladestrom und unter dem Maximal-Ladestrom des EVs liegt. Falls nicht, wird der Ladestrom auf den Mindest-Ladestrom bzw. den Maximal-Ladestrom des EV gesetzt. Wenn PV-Laden aktiv ist, darf die Stromstärke nicht unter den PV-Mindeststrom gesetzt werden. @@ -381,7 +392,10 @@ def check_min_max_current(self, # EV soll/darf nicht laden if required_current != 0: if not pv: - min_current = self.ev_template.data.min_current + if charging_type == ChargingType.AC.value: + min_current = self.ev_template.data.min_current + else: + min_current = self.ev_template.data.dc_min_current else: min_current = control_parameter.required_current if required_current < min_current: @@ -389,10 +403,13 @@ def check_min_max_current(self, msg = ("Die Einstellungen in dem Fahrzeug-Profil beschränken den Strom auf " f"mindestens {required_current} A.") else: - if phases == 1: - max_current = self.ev_template.data.max_current_single_phase + if charging_type == ChargingType.AC.value: + if phases == 1: + max_current = self.ev_template.data.max_current_single_phase + else: + max_current = self.ev_template.data.max_current_multi_phases else: - max_current = self.ev_template.data.max_current_multi_phases + max_current = self.ev_template.data.dc_max_current if required_current > max_current: required_current = max_current msg = ("Die Einstellungen in dem Fahrzeug-Profil beschränken den Strom auf " @@ -411,7 +428,7 @@ def _check_phase_switch_conditions(self, max_current_cp: int, limit: LimitingValue) -> Tuple[bool, Optional[str]]: # Manche EV laden mit 6.1A bei 6A Sollstrom - min_current = self.ev_template.data.min_current + self.ev_template.data.nominal_difference + min_current = control_parameter.min_current + self.ev_template.data.nominal_difference max_current = (min(self.ev_template.data.max_current_single_phase, max_current_cp) - self.ev_template.data.nominal_difference) phases_in_use = control_parameter.phases @@ -422,7 +439,7 @@ def _check_phase_switch_conditions(self, else: feed_in_yield = 0 all_surplus = data.data.counter_all_data.get_evu_counter().get_usable_surplus(feed_in_yield) - required_surplus = self.ev_template.data.min_current * max_phases_ev * 230 - get_power + required_surplus = control_parameter.min_current * max_phases_ev * 230 - get_power condition_1_to_3 = (((max(get_currents) > max_current and all_surplus > required_surplus) or limit == LimitingValue.UNBALANCED_LOAD.value) and phases_in_use == 1) @@ -462,11 +479,11 @@ def auto_phase_switch(self, if phases_in_use == 1: direction_str = f"Umschaltung von 1 auf {max_phases}" delay = cm_config.phase_switch_delay * 60 - required_reserved_power = (self.ev_template.data.min_current * max_phases * 230 - + required_reserved_power = (control_parameter.min_current * max_phases * 230 - self.ev_template.data.max_current_single_phase * 230) new_phase = max_phases - new_current = self.ev_template.data.min_current + new_current = control_parameter.min_current else: direction_str = f"Umschaltung von {max_phases} auf 1" delay = (16 - cm_config.phase_switch_delay) * 60 @@ -590,7 +607,8 @@ class ChargeTemplate: def time_charging(self, soc: Optional[float], - used_amount_time_charging: float) -> Tuple[int, str, Optional[str], Optional[str]]: + used_amount_time_charging: float, + charging_type: str) -> Tuple[int, str, Optional[str], Optional[str]]: """ prüft, ob ein Zeitfenster aktiv ist und setzt entsprechend den Ladestrom """ message = None @@ -598,22 +616,23 @@ def time_charging(self, if self.data.time_charging.plans: plan = timecheck.check_plans_timeframe(self.data.time_charging.plans) if plan is not None: + current = plan.current if charging_type == ChargingType.AC.value else plan.dc_current if self.data.et.active and data.data.optional_data.et_provider_availble(): if not data.data.optional_data.et_price_lower_than_limit(self.data.et.max_price): return 0, "stop", self.CHARGING_PRICE_EXCEEDED, plan.name if plan.limit.selected == "none": # kein Limit konfiguriert, mit konfigurierter Stromstärke laden - return plan.current, "time_charging", message, plan.name + return current, "time_charging", message, plan.name elif plan.limit.selected == "soc": # SoC Limit konfiguriert if soc: if soc < plan.limit.soc: - return plan.current, "time_charging", message, plan.name # Limit nicht erreicht + return current, "time_charging", message, plan.name # Limit nicht erreicht else: return 0, "stop", self.TIME_CHARGING_SOC_REACHED, plan.name # Limit erreicht else: return plan.current, "time_charging", message, plan.name elif plan.limit.selected == "amount": # Energiemengenlimit konfiguriert if used_amount_time_charging < plan.limit.amount: - return plan.current, "time_charging", message, plan.name # Limit nicht erreicht + return current, "time_charging", message, plan.name # Limit nicht erreicht else: return 0, "stop", self.TIME_CHARGING_AMOUNT_REACHED, plan.name # Limit erreicht else: @@ -633,28 +652,33 @@ def time_charging(self, def instant_charging(self, soc: Optional[float], - imported_instant_charging: float) -> Tuple[int, str, Optional[str]]: + imported_instant_charging: float, + charging_type: str) -> Tuple[int, str, Optional[str]]: """ prüft, ob die Lademengenbegrenzung erreicht wurde und setzt entsprechend den Ladestrom. """ message = None try: instant_charging = self.data.chargemode.instant_charging + if charging_type == ChargingType.AC.value: + current = instant_charging.current + else: + current = instant_charging.dc_current if self.data.et.active and data.data.optional_data.et_provider_availble(): if not data.data.optional_data.et_price_lower_than_limit(self.data.et.max_price): return 0, "stop", self.CHARGING_PRICE_EXCEEDED if instant_charging.limit.selected == "none": - return instant_charging.current, "instant_charging", message + return current, "instant_charging", message elif instant_charging.limit.selected == "soc": if soc: if soc < instant_charging.limit.soc: - return instant_charging.current, "instant_charging", message + return current, "instant_charging", message else: return 0, "stop", self.INSTANT_CHARGING_SOC_REACHED else: - return instant_charging.current, "instant_charging", message + return current, "instant_charging", message elif instant_charging.limit.selected == "amount": if imported_instant_charging < self.data.chargemode.instant_charging.limit.amount: - return instant_charging.current, "instant_charging", message + return current, "instant_charging", message else: return 0, "stop", self.INSTANT_CHARGING_AMOUNT_REACHED else: @@ -665,7 +689,7 @@ def instant_charging(self, PV_CHARGING_SOC_REACHED = "Keine Ladung, da der maximale Soc bereits erreicht wurde." - def pv_charging(self, soc: Optional[float], min_current: int) -> Tuple[int, str, Optional[str]]: + def pv_charging(self, soc: Optional[float], min_current: int, charging_type: str) -> Tuple[int, str, Optional[str]]: """ prüft, ob Min-oder Max-Soc erreicht wurden und setzt entsprechend den Ladestrom. """ message = None @@ -674,13 +698,21 @@ def pv_charging(self, soc: Optional[float], min_current: int) -> Tuple[int, str, 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: - return pv_charging.min_soc_current, "instant_charging", message - if pv_charging.min_current == 0: + if charging_type == ChargingType.AC.value: + current = pv_charging.min_soc_current + else: + current = pv_charging.dc_min_soc_current + return current, "instant_charging", message + 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: # nur PV; Ampere darf nicht 0 sein, wenn geladen werden soll return min_current, "pv_charging", message else: # Min PV - return pv_charging.min_current, "instant_charging", message + return pv_min_current, "instant_charging", message else: return 0, "stop", self.PV_CHARGING_SOC_REACHED except Exception: @@ -693,28 +725,36 @@ def scheduled_charging_recent_plan(self, phases: int, used_amount: float, max_phases: int, - phase_switch_supported: bool) -> Tuple[Optional[SelectedPlan], float]: + phase_switch_supported: bool, + charging_type: str) -> Tuple[Optional[SelectedPlan], float]: """ 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 and data.data.general_data.get_phases_chargemode("scheduled_charging", "instant_charging") == 0: - max_current = ev_template.data.max_current_multi_phases - plan_data = self.search_plan(max_current, soc, ev_template, max_phases, used_amount) - if plan_data: + if charging_type == ChargingType.AC.value: + 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, max_phases, used_amount, charging_type) + if plan_data and charging_type == ChargingType.AC.value: if 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) + 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 > 300: plan_data = plan_data_single_phase else: - if phases == 1: - max_current = ev_template.data.max_current_single_phase + 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.max_current_multi_phases - plan_data = self.search_plan(max_current, soc, ev_template, phases, used_amount) + 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, @@ -722,7 +762,8 @@ def search_plan(self, soc: Optional[float], ev_template: EvTemplate, phases: int, - used_amount: float) -> Optional[SelectedPlan]: + 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 @@ -733,7 +774,8 @@ def search_plan(self, raise ValueError("Um Zielladen mit SoC-Ziel nutzen zu können, bitte ein SoC-Modul konfigurieren " f"oder im Plan {plan.name} als Begrenzung Energie einstellen.") try: - duration, missing_amount = self.calculate_duration(plan, soc, battery_capacity, used_amount, phases) + duration, missing_amount = self.calculate_duration( + plan, soc, battery_capacity, used_amount, phases, charging_type) 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 @@ -748,9 +790,13 @@ def search_plan(self, (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=plan.current, + available_current=available_current, max_current=max_current, phases=phases, num=num, @@ -767,7 +813,8 @@ def calculate_duration(self, soc: Optional[float], battery_capacity: float, used_amount: float, - phases: int) -> Tuple[float, float]: + phases: int, + charging_type: str) -> Tuple[float, float]: if plan.limit.selected == "soc": if soc: missing_amount = ((plan.limit.soc_scheduled - soc) / 100) * battery_capacity @@ -775,7 +822,8 @@ def calculate_duration(self, raise ValueError("Um Zielladen mit SoC-Ziel nutzen zu können, bitte ein SoC-Modul konfigurieren.") else: missing_amount = plan.limit.amount - used_amount - duration = missing_amount/(plan.current * phases*230) * 3600 + current = plan.current if charging_type == ChargingType.AC.value else plan.dc_current + duration = missing_amount/(current * phases*230) * 3600 return duration, missing_amount SCHEDULED_REACHED_LIMIT_SOC = ("Kein Zielladen, da noch Zeit bis zum Zieltermin ist. " diff --git a/packages/control/ev_charge_template_test.py b/packages/control/ev_charge_template_test.py index c83775471a..cb60b28657 100644 --- a/packages/control/ev_charge_template_test.py +++ b/packages/control/ev_charge_template_test.py @@ -5,6 +5,7 @@ from control import data from control import optional +from control.chargepoint.charging_type import ChargingType from control.ev import ChargeTemplate, EvTemplate, EvTemplateData, SelectedPlan from control.general import General from helpermodules import timecheck @@ -60,7 +61,7 @@ def test_time_charging(plans: Dict[int, TimeChargingPlan], soc: float, used_amou monkeypatch.setattr(timecheck, "check_plans_timeframe", check_plans_timeframe_mock) # execution - ret = ct.time_charging(soc, used_amount_time_charging) + ret = ct.time_charging(soc, used_amount_time_charging, ChargingType.AC.value) # evaluation assert ret == expected @@ -86,7 +87,7 @@ def test_instant_charging(selected: str, current_soc: float, used_amount: float, ct.data.chargemode.instant_charging.limit.selected = selected # execution - ret = ct.instant_charging(current_soc, used_amount) + ret = ct.instant_charging(current_soc, used_amount, ChargingType.AC.value) # evaluation assert ret == expected @@ -110,7 +111,7 @@ def test_pv_charging(min_soc: int, min_current: int, current_soc: float, data.data.bat_all_data.data.config.configured = True # execution - ret = ct.pv_charging(current_soc, 6) + ret = ct.pv_charging(current_soc, 6, ChargingType.AC.value) # evaluation assert ret == expected @@ -151,7 +152,8 @@ def test_scheduled_charging_recent_plan(params: Params, monkeypatch): evt = Mock(spec=EvTemplate, data=evt_data) # execution - ct.scheduled_charging_recent_plan(50, evt, params.phases, 5, params.max_phases, params.phase_switch_supported) + ct.scheduled_charging_recent_plan(50, evt, params.phases, 5, params.max_phases, + params.phase_switch_supported, ChargingType.AC.value) # evaluation assert search_plan_mock.call_args.args[0] == params.expected_max_current @@ -170,7 +172,7 @@ def test_calculate_duration(selected: str, phases: int, expected_duration: float plan = ScheduledChargingPlan() plan.limit.selected = selected # execution - duration, missing_amount = ct.calculate_duration(plan, 60, 45000, 200, phases) + duration, missing_amount = ct.calculate_duration(plan, 60, 45000, 200, phases, ChargingType.AC.value) # evaluation assert duration == expected_duration @@ -199,7 +201,7 @@ def test_search_plan(check_duration_return1: Tuple[Optional[float], bool], plan_mock = Mock(spec=ScheduledChargingPlan, active=True, current=14, limit=Limit(selected="amount")) ct.data.chargemode.scheduled_charging.plans = {0: plan_mock, 1: plan_mock} # execution - plan_data = ct.search_plan(14, 60, EvTemplate(), 3, 200) + plan_data = ct.search_plan(14, 60, EvTemplate(), 3, 200, ChargingType.AC.value) # evaluation if expected_plan_num is None: diff --git a/packages/control/optional.py b/packages/control/optional.py index 6b0bd1f347..d6e4456188 100644 --- a/packages/control/optional.py +++ b/packages/control/optional.py @@ -7,6 +7,7 @@ from typing import Dict, List from dataclass_utils.factories import empty_dict_factory +from helpermodules import hardware_configuration from helpermodules.constants import NO_ERROR from helpermodules.pub import Pub from helpermodules.timecheck import create_unix_timestamp_current_full_hour @@ -75,6 +76,7 @@ class OptionalData: int_display: InternalDisplay = field(default_factory=int_display_factory) led: Led = field(default_factory=led_factory) rfid: Rfid = field(default_factory=rfid_factory) + dc_charging: bool = False class Optional: @@ -82,6 +84,8 @@ def __init__(self): try: self.data = OptionalData() self.et_module: ConfigurableElectricityTariff = None + self.data.dc_charging = hardware_configuration.get_hardware_configuration_setting("dc_charging") + Pub().pub("openWB/optional/dc_charging", self.data.dc_charging) except Exception: log.exception("Fehler im Optional-Modul") diff --git a/packages/helpermodules/abstract_plans.py b/packages/helpermodules/abstract_plans.py index 0e2967cfe8..b690c0a20f 100644 --- a/packages/helpermodules/abstract_plans.py +++ b/packages/helpermodules/abstract_plans.py @@ -62,6 +62,7 @@ class TimeframePlan(PlanBase): @dataclass class ScheduledChargingPlan(PlanBase): current: int = 14 + dc_current: float = 145 name: str = "neuer Zielladen-Plan" limit: ScheduledLimit = field(default_factory=scheduled_limit_factory) time: str = "07:00" # ToDo: aktuelle Zeit verwenden @@ -71,6 +72,7 @@ class ScheduledChargingPlan(PlanBase): class TimeChargingPlan(TimeframePlan): name: str = "neuer Zeitladen-Plan" current: int = 16 + dc_current: float = 145 limit: Limit = field(default_factory=limit_factory) diff --git a/packages/helpermodules/changed_values_handler_test.py b/packages/helpermodules/changed_values_handler_test.py index 2e9225f435..189912e814 100644 --- a/packages/helpermodules/changed_values_handler_test.py +++ b/packages/helpermodules/changed_values_handler_test.py @@ -1,16 +1,21 @@ from dataclasses import asdict, dataclass, field +from enum import IntEnum from typing import Dict, List, Optional, Tuple from unittest.mock import Mock import pytest -from control.chargepoint.chargepoint_state import ChargepointState from dataclass_utils.factories import currents_list_factory from helpermodules.changed_values_handler import ChangedValuesHandler NONE_TYPE = type(None) +class SampleIntEnum(IntEnum): + VALUE1 = 1 + VALUE2 = 2 + + @dataclass class SampleClass: parameter1: bool = False @@ -46,7 +51,7 @@ class SampleData: default_factory=sample_class, metadata={"topic": "get/field_class"}) sample_field_dict: Dict = field(default_factory=sample_dict_factory, metadata={ "topic": "get/field_dict"}) - sample_field_enum: ChargepointState = field(default=ChargepointState.CHARGING_ALLOWED, metadata={ + sample_field_enum: SampleIntEnum = field(default=SampleIntEnum.VALUE1, metadata={ "topic": "get/field_enum"}) sample_field_float: float = field(default=0, metadata={"topic": "get/field_float"}) sample_field_int: int = field(default=0, metadata={"topic": "get/field_int"}) @@ -77,8 +82,8 @@ class Params: expected_pub_call=("openWB/get/field_class", asdict(SampleClass(parameter1=True)))), Params(name="change dict", sample_data=SampleData(sample_field_dict={"key": "another_value"}), expected_pub_call=("openWB/get/field_dict", {"key": "another_value"})), - Params(name="change enum", sample_data=SampleData(sample_field_enum=ChargepointState.NO_CHARGING_ALLOWED), - expected_pub_call=("openWB/get/field_enum", ChargepointState.NO_CHARGING_ALLOWED.value)), + Params(name="change enum", sample_data=SampleData(sample_field_enum=SampleIntEnum.VALUE2), + expected_pub_call=("openWB/get/field_enum", SampleIntEnum.VALUE2.value)), Params(name="change float", sample_data=SampleData(sample_field_float=2.5), expected_pub_call=("openWB/get/field_float", 2.5)), Params(name="change int", sample_data=SampleData(sample_field_int=2), @@ -98,7 +103,7 @@ class Params: @pytest.mark.parametrize("params", cases, ids=[c.name for c in cases]) -def test_update_value(params: Params, mock_pub: Mock): +def test_update_value(params: Params, mock_pub: Mock, monkeypatch): # setup handler = ChangedValuesHandler(Mock()) @@ -106,6 +111,6 @@ def test_update_value(params: Params, mock_pub: Mock): handler._update_value("openWB/", SampleData(), params.sample_data) # evaluation - assert len(mock_pub.method_calls) == params.expected_calls + assert len(mock_pub.method_calls) - 1 == params.expected_calls if params.expected_calls > 0: - assert mock_pub.method_calls[0].args == params.expected_pub_call + assert mock_pub.method_calls[1].args == params.expected_pub_call diff --git a/packages/helpermodules/hardware_configuration.py b/packages/helpermodules/hardware_configuration.py index cb81289693..179a88eb18 100644 --- a/packages/helpermodules/hardware_configuration.py +++ b/packages/helpermodules/hardware_configuration.py @@ -7,23 +7,30 @@ HARDWARE_CONFIGURATION_FILE = "/home/openwb/configuration.json" -def update_hardware_configuration(new_setting: Dict) -> None: +def _read_configuration() -> Dict: with open(HARDWARE_CONFIGURATION_FILE, "r") as f: - data = json.loads(f.read()) - write_and_check(HARDWARE_CONFIGURATION_FILE, data.update(new_setting)) + return json.loads(f.read()) + + +def update_hardware_configuration(new_setting: Dict) -> None: + data = _read_configuration() + data.update(new_setting) + write_and_check(HARDWARE_CONFIGURATION_FILE, data) def remove_setting_hardware_configuration(obsolet_setting: str) -> None: - with open(HARDWARE_CONFIGURATION_FILE, "r") as f: - data = json.loads(f.read()) + data = _read_configuration() if obsolet_setting in data: - write_and_check(HARDWARE_CONFIGURATION_FILE, data.pop(obsolet_setting)) + data.pop(obsolet_setting) + write_and_check(HARDWARE_CONFIGURATION_FILE, data) def get_hardware_configuration_setting(name: str, default=None): - with open(HARDWARE_CONFIGURATION_FILE, "r") as f: - configuration = json.loads(f.read()) - return configuration.get(name, default) + return _read_configuration().get(name, default) + + +def exists_hardware_configuration_setting(name: str) -> bool: + return name in _read_configuration() def get_serial_number() -> str: diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 1af3fe797b..27431294df 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -10,7 +10,7 @@ import paho.mqtt.client as mqtt import logging -from helpermodules import subdata +from helpermodules import hardware_configuration, subdata from helpermodules.broker import InternalBrokerClient from helpermodules.pub import Pub, pub_single from helpermodules.utils.topic_parser import (decode_payload, get_index, get_index_position, get_second_index, @@ -452,6 +452,8 @@ def _subprocess_vehicle_chargemode_topic(self, msg: mqtt.MQTTMessage): 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: @@ -463,10 +465,14 @@ def _subprocess_vehicle_chargemode_topic(self, msg: mqtt.MQTTMessage): 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: @@ -521,7 +527,10 @@ def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): "/set/charging_ev_prev" in msg.topic): self._validate_value(msg, int, [(-1, float("inf"))]) elif "/set/current" in msg.topic: - self._validate_value(msg, float, [(6, 32), (0, 0)]) + if hardware_configuration.get_hardware_configuration_setting("dc_charging"): + self._validate_value(msg, float, [(0, 0), (6, 32), (0, 450)]) + else: + self._validate_value(msg, float, [(6, 32), (0, 0)]) elif ("/set/energy_to_charge" in msg.topic or "/set/required_power" in msg.topic): self._validate_value(msg, float, [(0, float("inf"))]) @@ -545,7 +554,10 @@ def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): elif "get" in msg.topic: self.process_chargepoint_get_topics(msg) elif "/control_parameter/required_current" in msg.topic: - self._validate_value(msg, float, [(6, 32), (0, 0)]) + if hardware_configuration.get_hardware_configuration_setting("dc_charging"): + self._validate_value(msg, float, [(0, 0), (6, 32), (0, 450)]) + else: + self._validate_value(msg, float, [(6, 32), (0, 0)]) elif "/control_parameter/phases" in msg.topic: self._validate_value(msg, int, [(0, 3)]) elif "/control_parameter/failed_phase_switches" in msg.topic: @@ -560,8 +572,10 @@ def process_chargepoint_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, str) 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 "/control_parameter/timestamp_switch_on_off" in msg.topic or "/control_parameter/timestamp_auto_phase_switch" in msg.topic or + "/control_parameter/timestamp_charge_start" in msg.topic or "/control_parameter/timestamp_perform_phase_switch" in msg.topic): self._validate_value(msg, float, [(0, float("inf"))]) elif "/control_parameter/state" in msg.topic: @@ -588,6 +602,9 @@ def process_chargepoint_get_topics(self, msg): elif ("/get/daily_imported" in msg.topic or "/get/daily_exported" in msg.topic or "/get/power" in msg.topic or + "/get/charging_current" in msg.topic or + "/get/charging_power" in msg.topic or + "/get/charging_voltage" in msg.topic or "/get/imported" in msg.topic or "/get/exported" in msg.topic or "/get/soc_timestamp" in msg.topic): @@ -610,10 +627,12 @@ def process_chargepoint_get_topics(self, msg): "/get/vehicle_id" in msg.topic or "/get/serial_number" in msg.topic): self._validate_value(msg, str) - elif "/get/rfid_timestamp" in msg.topic: - self._validate_value(msg, float) elif ("/get/soc" in msg.topic): self._validate_value(msg, float, [(0, 100)]) + elif "/get/rfid_timestamp" in msg.topic: + self._validate_value(msg, float) + elif "/get/simulation" in msg.topic: + self._validate_value(msg, "json") else: self.__unknown_topic(msg) @@ -866,7 +885,7 @@ def process_counter_topic(self, msg: mqtt.MQTTMessage): enthält Topic und Payload """ try: - if ("openWB/set/counter/config/reserve_for_not_charging" in msg.topic or + if ("openWB/set/counter/config/consider_less_charging" in msg.topic or "openWB/set/counter/set/loadmanagement_active" in msg.topic): self._validate_value(msg, bool) elif "openWB/set/counter/set/invalid_home_consumption" in msg.topic: diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index d8bc88df2b..728e2e3cab 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -425,13 +425,18 @@ def process_chargepoint_topic(self, var: Dict[str, chargepoint.Chargepoint], 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) - elif re.search("/chargepoint/[0-9]+/get/", msg.topic) is not None: - if (re.search("/chargepoint/[0-9]+/get/soc$", msg.topic) is not None and - decode_payload(msg.payload) != var["cp"+index].chargepoint.data.get.soc): - # Wenn das Auto noch nicht zugeordnet ist, wird der SoC nach der Zuordnung aktualisiert - if var["cp"+index].chargepoint.data.set.charging_ev > -1: - Pub().pub(f'openWB/set/vehicle/{var["cp"+index].chargepoint.data.set.charging_ev}' - '/get/force_soc_update', True) + elif (re.search("/chargepoint/[0-9]+/get/soc$", msg.topic) is not None and + decode_payload(msg.payload) != var["cp"+index].chargepoint.data.get.soc): + # Wenn das Auto noch nicht zugeordnet ist, wird der SoC nach der Zuordnung aktualisiert + if var["cp"+index].chargepoint.data.set.charging_ev > -1: + Pub().pub(f'openWB/set/vehicle/{var["cp"+index].chargepoint.data.set.charging_ev}' + '/get/force_soc_update', True) + self.set_json_payload_class(var["cp"+index].chargepoint.data.get, msg) + elif re.search("/chargepoint/[0-9]+/get/simulation$", msg.topic) is not None: + var["cp"+index].chargepoint.chargepoint_module.sim_counter.data = dataclass_from_dict( + SimCounterState, + decode_payload(msg.payload)) + else: self.set_json_payload_class(var["cp"+index].chargepoint.data.get, msg) elif re.search("/chargepoint/[0-9]+/config$", msg.topic) is not None: self.process_chargepoint_config_topic(var, msg) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index c636ba1c60..8a85a63b64 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -9,6 +9,7 @@ from typing import List, Optional from paho.mqtt.client import Client as MqttClient, MQTTMessage from control.bat_all import BatConsiderationMode +from control.chargepoint.charging_type import ChargingType from control.general import ChargemodeConfig import dataclass_utils @@ -42,7 +43,7 @@ class UpdateConfig: - DATASTORE_VERSION = 54 + DATASTORE_VERSION = 56 valid_topic = [ "^openWB/bat/config/configured$", "^openWB/bat/set/charging_power_left$", @@ -106,6 +107,7 @@ class UpdateConfig: "^openWB/chargepoint/[0-9]+/get/serial_number$", "^openWB/chargepoint/[0-9]+/get/soc$", "^openWB/chargepoint/[0-9]+/get/soc_timestamp$", + "^openWB/chargepoint/[0-9]+/get/simulation$", "^openWB/chargepoint/[0-9]+/get/state_str$", "^openWB/chargepoint/[0-9]+/get/connected_vehicle/soc$", "^openWB/chargepoint/[0-9]+/get/connected_vehicle/info$", @@ -136,7 +138,7 @@ class UpdateConfig: "^openWB/command/[A-Za-z0-9_]+/error$", "^openWB/command/todo$", - "^openWB/counter/config/reserve_for_not_charging$", + "^openWB/counter/config/consider_less_charging$", "^openWB/counter/config/home_consumption_source_id$", "^openWB/counter/get/hierarchy$", "^openWB/counter/set/disengageable_smarthome_power$", @@ -423,7 +425,7 @@ class UpdateConfig: ("openWB/chargepoint/get/power", 0), ("openWB/chargepoint/template/0", get_chargepoint_template_default()), ("openWB/counter/get/hierarchy", []), - ("openWB/counter/config/reserve_for_not_charging", counter_all.Config().reserve_for_not_charging), + ("openWB/counter/config/consider_less_charging", counter_all.Config().consider_less_charging), ("openWB/counter/config/home_consumption_source_id", counter_all.Config().home_consumption_source_id), ("openWB/vehicle/0/name", "Standard-Fahrzeug"), ("openWB/vehicle/0/charge_template", ev.Ev(0).charge_template.ct_num), @@ -1353,7 +1355,6 @@ def upgrade(topic: str, payload) -> Optional[dict]: return {topic: updated_payload} self._loop_all_received_topics(upgrade) self.__update_topic("openWB/system/datastore_version", 39) - Pub().pub("openWB/system/datastore_version", 39) def upgrade_datastore_39(self) -> None: def upgrade(topic: str, payload) -> None: @@ -1654,3 +1655,25 @@ def upgrade(topic: str, payload) -> Optional[dict]: return {topic: configuration_payload} self._loop_all_received_topics(upgrade) self.__update_topic("openWB/system/datastore_version", 54) + + def upgrade_datastore_54(self) -> None: + def upgrade(topic: str, payload) -> Optional[dict]: + if "openWB/counter/config/reserve_for_less_charging" == topic: + payload = decode_payload(payload) + return {"openWB/counter/config/consider_less_charging": payload} + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 55) + + def upgrade_datastore_55(self) -> None: + if hardware_configuration.exists_hardware_configuration_setting("dc_charging") is False: + hardware_configuration.update_hardware_configuration({"dc_charging": False}) + + def upgrade(topic: str, payload) -> Optional[dict]: + if re.search("openWB/chargepoint/template/[0-9]+", topic) is not None: + payload = decode_payload(payload) + if "charging_type" not in payload: + updated_payload = payload + updated_payload["charging_type"] = ChargingType.AC.value + return {topic: updated_payload} + self._loop_all_received_topics(upgrade) + self.__update_topic("openWB/system/datastore_version", 56) diff --git a/packages/modules/chargepoints/external_openwb/config.py b/packages/modules/chargepoints/external_openwb/config.py index 9f95f58ae9..50c322aeb5 100644 --- a/packages/modules/chargepoints/external_openwb/config.py +++ b/packages/modules/chargepoints/external_openwb/config.py @@ -1,5 +1,7 @@ from typing import Optional +from modules.common.abstract_chargepoint import SetupChargepoint + class OpenWBSeriesConfiguration: def __init__(self, ip_address: Optional[str] = None, duo_num: int = 0): @@ -7,13 +9,10 @@ def __init__(self, ip_address: Optional[str] = None, duo_num: int = 0): self.duo_num = duo_num -class OpenWBSeries: +class OpenWBSeries(SetupChargepoint[OpenWBSeriesConfiguration]): def __init__(self, name: str = "Externe openWB", type: str = "external_openwb", id: int = 0, configuration: OpenWBSeriesConfiguration = None) -> None: - self.name = name - self.type = type - self.id = id - self.configuration = configuration or OpenWBSeriesConfiguration() + super().__init__(name, type, id, configuration or OpenWBSeriesConfiguration()) diff --git a/packages/modules/chargepoints/internal_openwb/config.py b/packages/modules/chargepoints/internal_openwb/config.py index e8899b15c3..94f7f6253d 100644 --- a/packages/modules/chargepoints/internal_openwb/config.py +++ b/packages/modules/chargepoints/internal_openwb/config.py @@ -1,5 +1,7 @@ from enum import Enum +from modules.common.abstract_chargepoint import SetupChargepoint + class InternalChargepointMode(Enum): SOCKET = "socket" @@ -14,13 +16,10 @@ def __init__(self, mode: str = InternalChargepointMode.SERIES.value, duo_num: in self.duo_num = duo_num -class InternalOpenWB: +class InternalOpenWB(SetupChargepoint[InternalOpenWBConfiguration]): def __init__(self, name: str = "Interne openWB", type: str = "internal_openwb", id: int = 0, configuration: InternalOpenWBConfiguration = None) -> None: - self.name = name - self.type = type - self.id = id - self.configuration = configuration or InternalOpenWBConfiguration() + super().__init__(name, type, id, configuration or InternalOpenWBConfiguration()) diff --git a/packages/modules/chargepoints/mqtt/config.py b/packages/modules/chargepoints/mqtt/config.py index 0020bba750..7d96e7242f 100644 --- a/packages/modules/chargepoints/mqtt/config.py +++ b/packages/modules/chargepoints/mqtt/config.py @@ -1,15 +1,15 @@ +from modules.common.abstract_chargepoint import SetupChargepoint + + class MqttConfiguration: def __init__(self): pass -class Mqtt: +class Mqtt(SetupChargepoint[MqttConfiguration]): def __init__(self, name: str = "MQTT-Ladepunkt", type: str = "mqtt", id: int = 0, configuration: MqttConfiguration = None) -> None: - self.name = name - self.type = type - self.id = id - self.configuration = configuration or MqttConfiguration() + super().__init__(name, type, id, configuration or MqttConfiguration()) diff --git a/packages/modules/chargepoints/openwb_dc_adapter/__init__.py b/packages/modules/chargepoints/openwb_dc_adapter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/chargepoints/openwb_dc_adapter/chargepoint_module.py b/packages/modules/chargepoints/openwb_dc_adapter/chargepoint_module.py new file mode 100644 index 0000000000..3dcc7f4181 --- /dev/null +++ b/packages/modules/chargepoints/openwb_dc_adapter/chargepoint_module.py @@ -0,0 +1,125 @@ + +from enum import IntEnum +import logging + +from helpermodules import hardware_configuration +from helpermodules.utils.error_counter import ErrorCounterContext +from modules.chargepoints.openwb_dc_adapter.config import OpenWBDcAdapter +from modules.common.abstract_chargepoint import AbstractChargepoint +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_context import SingleComponentUpdateContext +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.simcount import SimCounterChargepoint +from modules.common.store import get_chargepoint_value_store +from modules.common.component_state import ChargepointState +from modules.common import req + +log = logging.getLogger(__name__) + + +class ChargingStatus(IntEnum): + AVAILABLE = 0 + PREPARING_TAGID_READY = 1 + PREPARING_EV_READY = 2 + CHARGING = 3 + SUSPENDED_EV = 4 + SUSPENDED_EVSE = 5 + FINISHING = 6 + RESERVED = 7 + UNAVAILABLE = 8 + UNAVAILABLE_FW_UPDATE = 9 + FAULTED = 10 + UNAVAILABLE_CONN_OBJ = 11 + + +class ChargepointModule(AbstractChargepoint): + def __init__(self, config: OpenWBDcAdapter) -> None: + self.config = config + self.store = get_chargepoint_value_store(self.config.id) + self.fault_state = FaultState(ComponentInfo( + self.config.id, + "Ladepunkt", "chargepoint")) + self.sim_counter = SimCounterChargepoint(self.config.id) + self.__session = req.get_http_session() + self.__client_error_context = ErrorCounterContext( + "Anhaltender Fehler beim Auslesen des Ladepunkts. Sollstromstärke wird zurückgesetzt.") + if hardware_configuration.get_hardware_configuration_setting("openwb_dc_adapter") is False: + raise Exception( + "DC-Laden muss durch den Support freigeschaltet werden. Bitte nehme Kontakt mit dem Support auf.") + self.efficiency = None + + with SingleComponentUpdateContext(self.fault_state, False): + with self.__client_error_context: + self.__session.post( + 'http://' + self.config.configuration.ip_address + '/connect.php', + data={'heartbeatenabled': '1'}) + + def set_current(self, current: float) -> None: + if self.__client_error_context.error_counter_exceeded(): + current = 0 + with SingleComponentUpdateContext(self.fault_state, False): + with self.__client_error_context: + ip_address = self.config.configuration.ip_address + raw_current = self.subtract_conversion_loss_from_current(current) + raw_power = raw_current * 3 * 230 + log.debug(f"DC-Stromstärke: {raw_current}A ≙ {raw_power / 1000}kW") + self.__session.post('http://'+ip_address+'/connect.php', data={'power': raw_power}) + + def subtract_conversion_loss_from_current(self, current: float) -> float: + return current * (self.efficiency if self.efficiency else 0.9) + + def add_conversion_loss_to_current(self, current: float) -> float: + return current / (self.efficiency if self.efficiency else 0.9) + + def get_values(self) -> None: + with SingleComponentUpdateContext(self.fault_state): + with self.__client_error_context: + ip_address = self.config.configuration.ip_address + json_rsp = self.__session.get('http://'+ip_address+'/connect.php').json() + + if json_rsp["fault_state"] == 1: + self.fault_state.warning(json_rsp["fault_str"]) + elif json_rsp["fault_state"] == 2: + raise Exception(json_rsp["fault_str"]) + + charging_power = json_rsp["charging_power"] + imported, exported = self.sim_counter.sim_count(charging_power) + chargepoint_state = ChargepointState( + charge_state=json_rsp["charge_state"], + charging_current=json_rsp["charging_current"], + charging_power=charging_power, + charging_voltage=json_rsp["charging_voltage"], + currents=json_rsp["currents"], + exported=exported, + imported=imported, + phases_in_use=3, + power=json_rsp["power_all"], + powers=json_rsp["powers"], + plug_state=json_rsp["plug_state"], + rfid=json_rsp["rfid_tag"], + soc=json_rsp["soc_value"], + soc_timestamp=json_rsp["soc_timestamp"], + vehicle_id=json_rsp["vehicle_id"], + serial_number=json_rsp["serial"], + ) + + if chargepoint_state.charge_state: + try: + self.efficiency = chargepoint_state.charging_power / chargepoint_state.power + log.debug(f"Effizienz: {self.efficiency}") + except ZeroDivisionError: + self.efficiency = None + else: + self.efficiency = None + if not (json_rsp["state"] == ChargingStatus.AVAILABLE.value or + json_rsp["state"] == ChargingStatus.PREPARING_TAGID_READY.value or + json_rsp["state"] == ChargingStatus.PREPARING_EV_READY.value or + json_rsp["state"] == ChargingStatus.CHARGING.value or + json_rsp["state"] == ChargingStatus.FINISHING.value or + json_rsp["state"] == ChargingStatus.UNAVAILABLE_CONN_OBJ.value): + raise Exception(f"Ladepunkt nicht verfügbar. Status: {ChargingStatus(json_rsp['state'])}") + self.store.set(chargepoint_state) + self.__client_error_context.reset_error_counter() + + +chargepoint_descriptor = DeviceDescriptor(configuration_factory=OpenWBDcAdapter) diff --git a/packages/modules/chargepoints/openwb_dc_adapter/config.py b/packages/modules/chargepoints/openwb_dc_adapter/config.py new file mode 100644 index 0000000000..c29770895b --- /dev/null +++ b/packages/modules/chargepoints/openwb_dc_adapter/config.py @@ -0,0 +1,20 @@ +from typing import Optional + +from helpermodules import hardware_configuration +from modules.common.abstract_chargepoint import SetupChargepoint + + +class OpenWBDcAdapterConfiguration: + def __init__(self, ip_address: Optional[str] = None): + self.ip_address = ip_address + + +class OpenWBDcAdapter(SetupChargepoint[OpenWBDcAdapterConfiguration]): + def __init__(self, + name: str = "openWB Adapter für DC-Lader", + type: str = "openwb_dc_adapter", + id: int = 0, + configuration: OpenWBDcAdapterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or OpenWBDcAdapterConfiguration()) + self.visibility = hardware_configuration.get_hardware_configuration_setting("dc_charging") + self.charging_type = "DC" diff --git a/packages/modules/chargepoints/openwb_pro/config.py b/packages/modules/chargepoints/openwb_pro/config.py index b42a55be68..db54fe1672 100644 --- a/packages/modules/chargepoints/openwb_pro/config.py +++ b/packages/modules/chargepoints/openwb_pro/config.py @@ -1,5 +1,7 @@ from typing import Optional +from modules.common.abstract_chargepoint import SetupChargepoint + class OpenWBProConfiguration: def __init__(self, ip_address: Optional[str] = None, duo_num: int = 0): @@ -7,13 +9,10 @@ def __init__(self, ip_address: Optional[str] = None, duo_num: int = 0): self.duo_num = duo_num -class OpenWBPro: +class OpenWBPro(SetupChargepoint[OpenWBProConfiguration]): def __init__(self, name: str = "openWB Pro", type: str = "openwb_pro", id: int = 0, configuration: OpenWBProConfiguration = None) -> None: - self.name = name - self.type = type - self.id = id - self.configuration = configuration or OpenWBProConfiguration() + super().__init__(name, type, id, configuration or OpenWBProConfiguration()) diff --git a/packages/modules/chargepoints/openwb_series2_satellit/config.py b/packages/modules/chargepoints/openwb_series2_satellit/config.py index 2dd674da6d..b331089349 100644 --- a/packages/modules/chargepoints/openwb_series2_satellit/config.py +++ b/packages/modules/chargepoints/openwb_series2_satellit/config.py @@ -1,5 +1,7 @@ from typing import Optional +from modules.common.abstract_chargepoint import SetupChargepoint + class OpenWBseries2SatellitConfiguration: def __init__(self, ip_address: Optional[str] = None, duo_num: int = 0): @@ -7,13 +9,10 @@ def __init__(self, ip_address: Optional[str] = None, duo_num: int = 0): self.duo_num = duo_num -class OpenWBseries2Satellit: +class OpenWBseries2Satellit(SetupChargepoint[OpenWBseries2SatellitConfiguration]): def __init__(self, name: str = "openWB series2 satellit, openWB series2 satellit Duo", type: str = "openwb_series2_satellit", id: int = 0, configuration: OpenWBseries2SatellitConfiguration = None) -> None: - self.name = name - self.type = type - self.id = id - self.configuration = configuration or OpenWBseries2SatellitConfiguration() + super().__init__(name, type, id, configuration or OpenWBseries2SatellitConfiguration()) diff --git a/packages/modules/chargepoints/smartwb/config.py b/packages/modules/chargepoints/smartwb/config.py index 06c16e2dc9..8052737999 100644 --- a/packages/modules/chargepoints/smartwb/config.py +++ b/packages/modules/chargepoints/smartwb/config.py @@ -1,5 +1,7 @@ from typing import Optional +from modules.common.abstract_chargepoint import SetupChargepoint + class SmartWBConfiguration: def __init__(self, ip_address: Optional[str] = None, timeout: int = 2): @@ -7,13 +9,10 @@ def __init__(self, ip_address: Optional[str] = None, timeout: int = 2): self.timeout = timeout -class SmartWB: +class SmartWB(SetupChargepoint[SmartWBConfiguration]): def __init__(self, name: str = "smartWB / EVSE-Wifi (>= v1.x.x/v2.x.x)", type: str = "smartwb", id: int = 0, configuration: SmartWBConfiguration = None) -> None: - self.name = name - self.type = type - self.id = id - self.configuration = configuration or SmartWBConfiguration() + super().__init__(name, type, id, configuration or SmartWBConfiguration()) diff --git a/packages/modules/common/abstract_chargepoint.py b/packages/modules/common/abstract_chargepoint.py index 1b7b1ac10f..140cdabc9e 100644 --- a/packages/modules/common/abstract_chargepoint.py +++ b/packages/modules/common/abstract_chargepoint.py @@ -1,4 +1,7 @@ from abc import abstractmethod +from typing import Generic, TypeVar + +from control.chargepoint.charging_type import ChargingType class AbstractChargepoint: @@ -25,3 +28,30 @@ def interrupt_cp(self, duration: int) -> None: @abstractmethod def clear_rfid(self) -> None: pass + + @abstractmethod + def add_conversion_loss_to_current(self, current: float) -> float: + return current + + @abstractmethod + def subtract_conversion_loss_from_current(self, current: float) -> float: + return current + + +T = TypeVar("T") + + +class SetupChargepoint(Generic[T]): + def __init__(self, + name: str, + type: str, + id: int, + configuration: T, + charging_type: str = ChargingType.AC.value, + visibility: bool = True) -> None: + self.name = name + self.type = type + self.id = id + self.configuration = configuration + self.charging_type = charging_type + self.visibility = visibility diff --git a/packages/modules/common/component_state.py b/packages/modules/common/component_state.py index afcd76d0bf..fa2a904658 100644 --- a/packages/modules/common/component_state.py +++ b/packages/modules/common/component_state.py @@ -149,6 +149,9 @@ def __init__(self, exported: float = 0, power: float = 0, serial_number: str = "", + charging_current: Optional[float] = 0, + charging_voltage: Optional[float] = 0, + charging_power: Optional[float] = 0, powers: Optional[List[Optional[float]]] = None, voltages: Optional[List[Optional[float]]] = None, currents: Optional[List[Optional[float]]] = None, @@ -178,6 +181,9 @@ def __init__(self, self.rfid_timestamp = rfid_timestamp if _check_none(power_factors): power_factors = [0.0]*3 + self.charging_current = charging_current + self.charging_power = charging_power + self.charging_voltage = charging_voltage self.power_factors = power_factors self.soc = soc self.soc_timestamp = soc_timestamp diff --git a/packages/modules/common/simcount/__init__.py b/packages/modules/common/simcount/__init__.py index add2e1ac5d..7c25745c47 100644 --- a/packages/modules/common/simcount/__init__.py +++ b/packages/modules/common/simcount/__init__.py @@ -1,3 +1,3 @@ from modules.common.simcount._simcount import sim_count -from modules.common.simcount._simcounter import SimCounter +from modules.common.simcount._simcounter import SimCounter, SimCounterChargepoint from modules.common.simcount.simcounter_state import SimCounterState diff --git a/packages/modules/common/simcount/_simcounter.py b/packages/modules/common/simcount/_simcounter.py index 0e65dec191..5f196c7643 100644 --- a/packages/modules/common/simcount/_simcounter.py +++ b/packages/modules/common/simcount/_simcounter.py @@ -13,3 +13,13 @@ def __init__(self, device_id: int, component_id: int, prefix: str): def sim_count(self, power: float) -> Tuple[float, float]: self.data = sim_count(power, self.topic, self.data, self.prefix) return self.data.imported, self.data.exported + + +class SimCounterChargepoint: + def __init__(self, chargepoint_id: int): + self.topic = f"openWB/set/chargepoint/{chargepoint_id}/get/" + self.data = None # type: Optional[SimCounterState] + + def sim_count(self, power: float) -> Tuple[float, float]: + self.data = sim_count(power, self.topic, self.data, "") + return self.data.imported, self.data.exported diff --git a/packages/modules/common/store/_chargepoint.py b/packages/modules/common/store/_chargepoint.py index 5f68602a9f..631040c511 100644 --- a/packages/modules/common/store/_chargepoint.py +++ b/packages/modules/common/store/_chargepoint.py @@ -28,6 +28,11 @@ def set(self, state: ChargepointState) -> None: self.state = state def update(self): + pub_to_broker("openWB/set/chargepoint/" + str(self.num) + + "/get/charging_current", self.state.charging_current, 2) + pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/charging_power", self.state.charging_power, 2) + pub_to_broker("openWB/set/chargepoint/" + str(self.num) + + "/get/charging_voltage", self.state.charging_voltage, 2) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/voltages", self.state.voltages, 2) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/currents", self.state.currents, 2) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/power_factors", self.state.power_factors, 2) diff --git a/packages/modules/configuration.py b/packages/modules/configuration.py index a1708e2b3f..5d3959e83d 100644 --- a/packages/modules/configuration.py +++ b/packages/modules/configuration.py @@ -228,10 +228,11 @@ def create_chargepoints_list(path_list): dev_defaults = importlib.import_module( f".chargepoints.{path.parts[-2]}.chargepoint_module", "modules").chargepoint_descriptor.configuration_factory() - chargepoints.append({ - "value": dev_defaults.type, - "text": dev_defaults.name - }) + if dev_defaults.visibility: + chargepoints.append({ + "value": dev_defaults.type, + "text": dev_defaults.name + }) except Exception: log.exception("Fehler im configuration-Modul") chargepoints = sorted(chargepoints, key=lambda d: d['text'].upper()) diff --git a/packages/modules/internal_chargepoint_handler/update_values_test.py b/packages/modules/internal_chargepoint_handler/update_values_test.py index 2360a31bcc..6fcd274076 100644 --- a/packages/modules/internal_chargepoint_handler/update_values_test.py +++ b/packages/modules/internal_chargepoint_handler/update_values_test.py @@ -24,7 +24,7 @@ @pytest.mark.parametrize( "old_chargepoint_state, published_topics", - [(None, 36), + [(None, 42), (OLD_CHARGEPOINT_STATE, 2)] ) diff --git a/packages/modules/update_soc.py b/packages/modules/update_soc.py index 68dda5ce6b..2b54411703 100644 --- a/packages/modules/update_soc.py +++ b/packages/modules/update_soc.py @@ -103,9 +103,12 @@ def _get_vehicle_update_data(self, ev_num: int) -> VehicleUpdateData: if ev.soc_module.general_config.use_soc_from_cp: soc_from_cp = cp.data.get.soc timestamp_soc_from_cp = cp.data.get.soc_timestamp + log.debug(f"cp.num {cp.num} cp.data.get {cp.data.get}") + log.debug(f"1 soc_from_cp: {soc_from_cp}, timestamp_soc_from_cp: {timestamp_soc_from_cp}") else: soc_from_cp = None timestamp_soc_from_cp = None + log.debug(f"2 soc_from_cp: {soc_from_cp}, timestamp_soc_from_cp: {timestamp_soc_from_cp}") break else: plug_state = False @@ -115,6 +118,7 @@ def _get_vehicle_update_data(self, ev_num: int) -> VehicleUpdateData: efficiency = ev_template.data.efficiency soc_from_cp = None timestamp_soc_from_cp = None + log.debug(f"3 soc_from_cp: {soc_from_cp}, timestamp_soc_from_cp: {timestamp_soc_from_cp}") return VehicleUpdateData(plug_state=plug_state, charge_state=charge_state, efficiency=efficiency, diff --git a/packages/modules/web_themes/standard_legacy/web/index.html b/packages/modules/web_themes/standard_legacy/web/index.html index 6f0a09dd7e..e82a11b1ba 100644 --- a/packages/modules/web_themes/standard_legacy/web/index.html +++ b/packages/modules/web_themes/standard_legacy/web/index.html @@ -638,6 +638,29 @@

Einstellungen für "Sofortladen"

+
+
+
+ DC-Sollleistung +
+
+
+
+ +
+ +
+
+
+
@@ -720,6 +743,29 @@

Einstellungen für "PV"

+
+
+
+ Minimale DC-Dauerleistung +
+
+
+
+ +
+ +
+
+
+
Mindest-SoC @@ -759,6 +805,29 @@

Einstellungen für "PV"

+
+
+
+ DC Mindest-SoC-Leistung +
+
+
+
+ +
+ +
+
+
+
SoC-Limit diff --git a/packages/modules/web_themes/standard_legacy/web/processAllMqttMsg.js b/packages/modules/web_themes/standard_legacy/web/processAllMqttMsg.js index 346b2229d4..3d31317d39 100644 --- a/packages/modules/web_themes/standard_legacy/web/processAllMqttMsg.js +++ b/packages/modules/web_themes/standard_legacy/web/processAllMqttMsg.js @@ -55,14 +55,20 @@ function createChargePoint(hierarchy) { clonedElement.find('.card-body').attr('id', 'collapseChargepoint' + chargePointIndex).removeClass('show'); clonedElement.find('label[for=minCurrentPvCpT]').attr('for', 'minCurrentPvCp' + chargePointIndex); clonedElement.find('#minCurrentPvCpT').attr('id', 'minCurrentPvCp' + chargePointIndex); + clonedElement.find('label[for=minDcCurrentPvCpT]').attr('for', 'minDcCurrentPvCp' + chargePointIndex); + clonedElement.find('#minDcCurrentPvCpT').attr('id', 'minDcCurrentPvCp' + chargePointIndex); clonedElement.find('label[for=minSocPvCpT]').attr('for', 'minSocPvCp' + chargePointIndex); clonedElement.find('#minSocPvCpT').attr('id', 'minSocPvCp' + chargePointIndex); clonedElement.find('label[for=maxSocPvCpT]').attr('for', 'maxSocPvCp' + chargePointIndex); clonedElement.find('#maxSocPvCpT').attr('id', 'maxSocPvCp' + chargePointIndex); clonedElement.find('label[for=minSocCurrentPvCpT]').attr('for', 'minSocCurrentPvCp' + chargePointIndex); clonedElement.find('#minSocCurrentPvCpT').attr('id', 'minSocCurrentPvCp' + chargePointIndex); + clonedElement.find('label[for=minSocDcCurrentPvCpT]').attr('for', 'minSocDcCurrentPvCp' + chargePointIndex); + clonedElement.find('#minSocDcCurrentPvCpT').attr('id', 'minSocDcCurrentPvCp' + chargePointIndex); clonedElement.find('label[for=currentInstantChargeCpT]').attr('for', 'currentInstantChargeCp' + chargePointIndex); clonedElement.find('#currentInstantChargeCpT').attr('id', 'currentInstantChargeCp' + chargePointIndex); + clonedElement.find('label[for=dcCurrentInstantChargeCpT]').attr('for', 'dcCurrentInstantChargeCpT' + chargePointIndex); + clonedElement.find('#dcCurrentInstantChargeCpT').attr('id', 'dcCurrentInstantChargeCpT' + chargePointIndex); clonedElement.find('label[for=limitInstantChargeCpT]').attr('for', 'limitInstantChargeCp' + chargePointIndex); clonedElement.find('#limitInstantChargeCpT').attr('id', 'limitInstantChargeCp' + chargePointIndex); clonedElement.find('label[for=soclimitCpT]').attr('for', 'soclimitCp' + chargePointIndex); @@ -115,6 +121,9 @@ function refreshChargeTemplate(templateIndex) { // chargemode.instant_charging.current element = parent.find('.charge-point-instant-charge-current'); setInputValue(element.attr('id'), chargeModeTemplate[templateIndex].chargemode.instant_charging.current); + // chargemode.instant_charging.dc_current + element = parent.find('.charge-point-instant-charge-dc-current'); + setInputValue(element.attr('id'), chargeModeTemplate[templateIndex].chargemode.instant_charging.dc_current); // chargemode.instant_charging.limit.selected element = parent.find('.charge-point-instant-charge-limit-selected'); setToggleBtnGroup(element.attr('id'), chargeModeTemplate[templateIndex].chargemode.instant_charging.limit.selected); @@ -145,6 +154,9 @@ function refreshChargeTemplate(templateIndex) { // chargemode.pv_charging.min_current element = parent.find('.charge-point-pv-charge-min-current'); setInputValue(element.attr('id'), chargeModeTemplate[templateIndex].chargemode.pv_charging.min_current); + // chargemode.pv_charging.dc_min_current + element = parent.find('.charge-point-pv-charge-dc-min-current'); + setInputValue(element.attr('id'), chargeModeTemplate[templateIndex].chargemode.pv_charging.dc_min_current); // chargemode.pv_charging.min_soc element = parent.find('.charge-point-pv-charge-min-soc'); setInputValue(element.attr('id'), chargeModeTemplate[templateIndex].chargemode.pv_charging.min_soc); @@ -154,6 +166,9 @@ function refreshChargeTemplate(templateIndex) { // chargemode.pv_charging.min_soc_current element = parent.find('.charge-point-pv-charge-min-soc-current'); setInputValue(element.attr('id'), chargeModeTemplate[templateIndex].chargemode.pv_charging.min_soc_current); + // chargemode.pv_charging.dc_min_soc_current + element = parent.find('.charge-point-pv-charge-dc-min-soc-current'); + setInputValue(element.attr('id'), chargeModeTemplate[templateIndex].chargemode.pv_charging.dc_min_soc_current); // chargemode.pv_charging.feed_in_limit var element = parent.find('.charge-point-pv-charge-feed-in-limit'); // now get parents respective child element if (chargeModeTemplate[templateIndex].chargemode.pv_charging.feed_in_limit == true) { @@ -380,6 +395,7 @@ function handleMessage(mqttTopic, mqttPayload) { else if (mqttTopic.match(/^openwb\/general\/chargemode_config\/pv_charging\//i)) { processPvConfigMessages(mqttTopic, mqttPayload); } else if (mqttTopic.match(/^openwb\/graph\//i)) { processGraphMessages(mqttTopic, mqttPayload); } else if (mqttTopic.match(/^openwb\/optional\/et\//i)) { processETProviderMessages(mqttTopic, mqttPayload); } + else if (mqttTopic.match(/^openwb\/optional\//i)) { processOptionalMessages(mqttTopic, mqttPayload); } else if (mqttTopic.match(/^openwb\/LegacySmartHome\//i)) { processSmartHomeDeviceMessages(mqttTopic, mqttPayload); } } // end handleMessage @@ -1071,6 +1087,17 @@ function processETProviderMessages(mqttTopic, mqttPayload) { } } +function processOptionalMessages(mqttTopic, mqttPayload) { + if (mqttTopic == 'openWB/optional/dc_charging') { + data = JSON.parse(mqttPayload); + if (data == true) { + $('.dc-charging-configured').removeClass('hide'); + } else { + $('.dc-charging-configured').addClass('hide'); + } + } +} + function processSmartHomeDeviceMessages(mqttTopic, mqttPayload) { processPreloader(mqttTopic); var deviceIndex = getIndex(mqttTopic); // extract number between two / / diff --git a/packages/modules/web_themes/standard_legacy/web/setupMqttServices.js b/packages/modules/web_themes/standard_legacy/web/setupMqttServices.js index e4f30f6cdb..7853d831e3 100644 --- a/packages/modules/web_themes/standard_legacy/web/setupMqttServices.js +++ b/packages/modules/web_themes/standard_legacy/web/setupMqttServices.js @@ -77,6 +77,7 @@ var topicsToSubscribe = [ ["openWB/optional/et/active", 1], // et provider is configured ["openWB/optional/et/provider", 1], // et provider information ["openWB/optional/et/get/prices", 1], // current price list + ["openWB/optional/dc_charging", 1], // dc charging is configured // graph topics ["openWB/graph/config/duration", 1], // maximum duration to display in landing page