Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions packages/control/algorithm/integration_test/pv_charging_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ def assert_counter_set(params: ParamsExpectedCounterSet):


def test_start_pv_delay(all_cp_pv_charging_3p, all_cp_not_charging, monkeypatch):
# alle 3 im PV-laden, keine Ladung -> bei zwei die Verzögerung starten, für den 3. reicht es nicht
# alle 3 im PV-laden, keine Ladung, Überschuss reicht für alle 3 -> Verzögerung startet für alle 3.
# Die Einschaltschwelle wird seit PR #3899 bewusst immer für eine Phase berechnet (nicht mehr für die
# zuvor unconditional auf 1 gesetzte, hier aber unveränderte control_parameter.phases=3), daher reicht
# derselbe Überschuss jetzt für alle 3 statt nur für 2 von 3.
# setup
data.data.counter_data["counter0"].data.set.raw_power_left = 32090
data.data.counter_data["counter0"].data.set.raw_currents_left = [32, 30, 31]
Expand All @@ -140,7 +143,7 @@ def test_start_pv_delay(all_cp_pv_charging_3p, all_cp_not_charging, monkeypatch)
assert data.data.cp_data[f"cp{i}"].data.set.current == 0
assert data.data.consumer_data["consumer7"].data.set.current == 0
assert data.data.cp_data[
"cp3"].data.control_parameter.timestamp_switch_on_off is None
"cp3"].data.control_parameter.timestamp_switch_on_off == 1652683252.0
assert data.data.cp_data[
"cp4"].data.control_parameter.timestamp_switch_on_off == 1652683252.0
assert data.data.cp_data[
Expand All @@ -149,7 +152,7 @@ def test_start_pv_delay(all_cp_pv_charging_3p, all_cp_not_charging, monkeypatch)
"consumer7"].data.control_parameter.timestamp_switch_on_off == 1652683252.0
assert data.data.counter_data["counter0"].data.set.raw_power_left == 32090
assert data.data.counter_data["counter0"].data.set.surplus_power_left == -690
assert data.data.counter_data["counter0"].data.set.reserved_surplus == 9115
assert data.data.counter_data["counter0"].data.set.reserved_surplus == 4615.0


def test_pv_delay_expired(all_cp_pv_charging_3p, all_cp_not_charging, monkeypatch):
Expand Down Expand Up @@ -182,6 +185,9 @@ def test_pv_delay_expired(all_cp_pv_charging_3p, all_cp_not_charging, monkeypatc
assert data.data.cp_data["cp3"].data.set.current == 10
assert data.data.cp_data["cp4"].data.set.current == 0
assert data.data.cp_data["cp5"].data.set.current == 0
# switch_on_timer_expired() setzt die Phasenzahl jetzt explizit (PR #3899, statt sich auf einen
# zuvor von außen gesetzten Wert zu verlassen) - bei ausreichend Überschuss direkt auf max_phases.
assert data.data.cp_data["cp3"].data.control_parameter.phases == 3
assert data.data.cp_data[
"cp3"].data.control_parameter.timestamp_switch_on_off is None
assert data.data.cp_data[
Expand All @@ -190,7 +196,7 @@ def test_pv_delay_expired(all_cp_pv_charging_3p, all_cp_not_charging, monkeypatc
"cp5"].data.control_parameter.timestamp_switch_on_off is None
assert data.data.counter_data["counter0"].data.set.raw_power_left == 24185
assert data.data.counter_data["counter0"].data.set.surplus_power_left == -690
assert data.data.counter_data["counter0"].data.set.reserved_surplus == 0
assert data.data.counter_data["counter0"].data.set.reserved_surplus == 6000.0


cases_limit = [
Expand Down
19 changes: 9 additions & 10 deletions packages/control/chargepoint/chargepoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,6 @@ def initiate_phase_switch(self):
log.exception("Fehler in der Ladepunkt-Klasse von "+str(self.num))

def get_phases_by_selected_chargemode(self, phases_chargemode: int) -> int:
charging_ev = self.data.set.charging_ev_data
if self.data.get.evse_signaling == EvseSignaling.HLC:
phases = self.data.get.phases_in_use
elif self.data.config.auto_phase_switch_hw is False and self.data.get.charge_state:
Expand All @@ -475,16 +474,16 @@ def get_phases_by_selected_chargemode(self, phases_chargemode: int) -> int:
if self.data.get.charge_state:
phases = self.data.get.phases_in_use
else:
if ((not charging_ev.ev_template.data.prevent_phase_switch or
self.data.set.log.imported_since_plugged == 0) and
self.data.config.auto_phase_switch_hw):
phases = 1
# Vor Ladestart keine Phasenzahl erzwingen, sondern die bisherige beibehalten, bis
# switch_on_timer_expired() (counter.py) nach Ablauf der Einschaltverzögerung die
# endgültige Phasenzahl anhand des tatsächlichen Überschusses festlegt (siehe PR #3899:
# ein unconditional phases=1 hier löste eine unnötige Umschaltung noch vor jeder
# Einschaltverzögerung aus, wenn die Hardware zufällig noch auf mehr Phasen stand).
if self.data.set.phases_to_use != 0:
phases = self.data.set.phases_to_use
else:
if self.data.set.phases_to_use != 0:
phases = self.data.set.phases_to_use
else:
# phases_target
phases = self.data.config.connected_phases
# phases_target
phases = self.data.config.connected_phases
log.debug(f"Phasenzahl Lademodus: {phases}")
else:
if phases_chargemode == 0:
Expand Down
69 changes: 68 additions & 1 deletion packages/control/chargepoint/chargepoint_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@

from control import data
from control.chargelog import chargelog
from control.chargemode import Chargemode
from control.chargepoint.chargepoint import Chargepoint
from control.chargepoint.chargepoint_state import ChargepointState
from control.chargepoint.chargepoint_template import CpTemplate
from control.chargepoint.chargepoint_template import CpTemplate, get_chargepoint_template_default
from control.counter import Counter
from control.ev.ev import Ev
from modules.common.configurable_vehicle import ConfigurableVehicle
Expand Down Expand Up @@ -131,6 +132,33 @@ class Params:
charge_state=True,
phase_switch_required=True
),
# Reproduziert den in PR #3899 beschriebenen Bug: get_phases_by_selected_chargemode() erzwang vor
# get_phases_test.py's Fix unconditional phases=1 beim Ladestart, auch wenn die Hardware (Rest vom
# vorherigen Ladevorgang) noch auf 3 Phasen steht. Das führt hier - noch bevor überhaupt eine
# Einschaltverzögerung beginnt (state=NO_CHARGING_ALLOWED, charge_state=False) - bereits zu einer
# unnötigen Umschaltung 3->1.
Params(
name="Ladestart, Hardware noch auf 3 Phasen: unnötige Umschaltung ohne den Fix",
state=ChargepointState.NO_CHARGING_ALLOWED,
phases_to_use=1,
phases_in_use=3,
control_parameter_phases=1,
set_current=6,
set_current_prev=0,
charge_state=False,
phase_switch_required=True
),
Params(
name="Ladestart, Hardware noch auf 3 Phasen: keine Umschaltung mit dem Fix (Überschuss reicht)",
state=ChargepointState.NO_CHARGING_ALLOWED,
phases_to_use=3,
phases_in_use=3,
control_parameter_phases=3,
set_current=6,
set_current_prev=0,
charge_state=False,
phase_switch_required=False
),
]


Expand Down Expand Up @@ -158,6 +186,45 @@ def test_is_phase_switch_required(params: Params):
assert ret == params.phase_switch_required


def test_get_phases_and_is_phase_switch_required_no_switch_on_stale_hardware_phases(mock_data):
""" Exakte Werte aus einem realen Log (2026-08-08 12:35:43, PR #3899): CUPRA Born
(min_current=6, max_phases=3) wird an einem 3-phasigen Ladepunkt neu erkannt, Automatik-
Umschaltung, noch nicht ladend, Hardware steht noch auf 3 Phasen (Rest vom vorherigen
Ladevorgang). Vor dem Fix erzwang get_phases_by_selected_chargemode() hier unconditional
phases=1, was noch vor jeder Einschaltverzögerung (state=NO_CHARGING_ALLOWED) eine unnötige
3->1-Umschaltung auslöste ("LP 4: Umschaltung von 3 auf 1 Phase, dafür wird die Ladung
unterbrochen."). """
# setup
cp = Chargepoint(4, None)
cp.template = CpTemplate()
cp.template.data = get_chargepoint_template_default()
cp.data.set.charging_ev_data = Ev(0)
cp.data.config.connected_phases = 3
cp.data.config.auto_phase_switch_hw = True
cp.data.get.charge_state = False
cp.data.control_parameter.submode = Chargemode.PV_CHARGING
cp.data.control_parameter.state = ChargepointState.NO_CHARGING_ALLOWED
cp.data.set.phases_to_use = 3 # von _process_charge_stop() beim Abstecken auf phases_in_use gesetzt
cp.data.get.phases_in_use = 3
cp.data.set.log.imported_since_plugged = 0
cp.data.set.charge_template.data.chargemode.pv_charging.phases_to_use = 0
cp.data.set.charging_ev_data.ev_template.data.prevent_phase_switch = False
cp.data.set.charging_ev_data.ev_template.data.min_current = 6
cp.data.set.charging_ev_data.ev_template.data.max_phases = 3
cp.data.set.current = 6

# execution
phases = cp.get_phases_by_selected_chargemode(0)
cp.data.control_parameter.phases = phases

# evaluation: bisherige Phasenzahl (3) wird beibehalten statt auf 1 erzwungen
assert phases == 3
# Downstream-Kette: da control_parameter.phases jetzt korrekt 3 ist (statt 1, wie vor dem Fix),
# findet _is_phase_switch_required() keinen Mismatch zur Hardware mehr - genau der Aufruf, der in
# process.py nach dem Algorithmus die eigentliche Umschaltung 3->1 ausgelöst hätte.
assert cp._is_phase_switch_required() is False


@pytest.mark.parametrize(
"soc_module, reset_after_unplug, expected_calls, expected_pub_call",
[
Expand Down
5 changes: 3 additions & 2 deletions packages/control/chargepoint/get_phases_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ def __init__(self,
Params("auto phase before charge no hw switch 1", connected_phases=3, auto_phase_switch_hw=False,
prevent_phase_switch=False, chargemode_phases=0, phases_in_use=1, imported_since_plugged=0,
expected_phases=1, charge_state=False),
Params("auto phase use min phase at start", connected_phases=3, auto_phase_switch_hw=True,
Params("auto phase before charge start: keep current phase count, don't force 1 (PR #3899)",
connected_phases=3, auto_phase_switch_hw=True,
prevent_phase_switch=False, chargemode_phases=0, phases_in_use=3, imported_since_plugged=0,
expected_phases=1, charge_state=False),
expected_phases=3, charge_state=False),
]


Expand Down
24 changes: 15 additions & 9 deletions packages/control/counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,10 +403,12 @@ def calc_switch_on_power(self, switch_on_threshold: float) -> Tuple[float, float
def get_pv_config_py_load(self, load: Load) -> Tuple[float, float, float]:
surplus_config = data.data.general_data.data.chargemode_config.surplus
if isinstance(load, Chargepoint):
control_parameter = load.data.control_parameter
switch_on_delay = surplus_config.vehicle.switch_on_delay
switch_on_threshold = surplus_config.vehicle.switch_on_threshold * control_parameter.phases
power_to_reserve = surplus_config.vehicle.switch_on_threshold*control_parameter.phases
# Einschaltschwelle wird bewusst immer für eine Phase berechnet, unabhängig von der
# aktuellen/vorherigen Phasenzahl - switch_on_timer_expired() legt die endgültige Phasenzahl
# erst anhand des dann tatsächlich verfügbaren Überschusses fest.
switch_on_threshold = surplus_config.vehicle.switch_on_threshold
power_to_reserve = surplus_config.vehicle.switch_on_threshold
else:
switch_on_delay = surplus_config.consumer.switch_on_delay
switch_on_threshold = (load.data.control_parameter.required_current *
Expand Down Expand Up @@ -484,15 +486,19 @@ def switch_on_timer_expired(self, load: Load) -> None:

if isinstance(load, Chargepoint):
charging_ev_data = load.data.set.charging_ev_data
# bei ausreichend Überschuss direkt mit max. Phasen laden
ev_template = charging_ev_data.ev_template
max_phases_power = ev_template.data.min_current * ev_template.data.max_phases * 230
if (control_parameter.submode == Chargemode.PV_CHARGING and
load.data.set.charge_template.data.chargemode.pv_charging.phases_to_use == 0 and
load.hw_supports_phase_switch() and
self.get_usable_surplus() > max_phases_power):
control_parameter.phases = ev_template.data.max_phases
msg += texts.max_phases.format(ev_template.data.max_phases)
load.hw_supports_phase_switch()):
# Phasenzahl hier in jedem Fall explizit setzen, nicht dem Wert überlassen, den
# get_phases_by_selected_chargemode() vor der Einschaltverzögerung gesetzt hatte -
# bei ausreichend Überschuss direkt mit max. Phasen laden, sonst explizit einphasig.
max_phases_power = ev_template.data.min_current * ev_template.data.max_phases * 230
if self.get_usable_surplus() > max_phases_power:
control_parameter.phases = ev_template.data.max_phases
msg += texts.max_phases.format(ev_template.data.max_phases)
else:
control_parameter.phases = 1
elif isinstance(load, Consumer):
control_parameter.state = ChargepointState.CHARGING_ALLOWED
load.set_state_and_log(msg)
Expand Down
Loading