Skip to content
4 changes: 2 additions & 2 deletions packages/control/chargepoint/chargepoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def _process_charge_stop(self) -> None:
self.data.set.ocpp_transaction_id = None
# muss vor dem Zurücksetzen der control parameter aufgerufen werden
self.data.set.charging_ev_data.reset_phase_switch(self.data.control_parameter)
self.data.set.charging_ev_data.reset_phase_switch_delay(self.data.control_parameter, self.get_max_phase_hw())
self.data.set.charging_ev_data.reset_phase_switch_delay(self.data.control_parameter)
self.reset_control_parameter_at_charge_stop()
data.data.counter_all_data.get_evu_counter().reset_switch_on_off(self)
if self.data.get.plug_state is False and self.data.set.plug_state_prev is True:
Expand Down Expand Up @@ -664,7 +664,7 @@ def update(self, ev_list: Dict[str, Ev]) -> None:

if self.chargemode_changed or self.submode_changed:
data.data.counter_all_data.get_evu_counter().reset_switch_on_off(self)
charging_ev.reset_phase_switch_delay(self.data.control_parameter, self.get_max_phase_hw())
charging_ev.reset_phase_switch_delay(self.data.control_parameter)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Disabled automation strands phase delay

When the automatic minimum-SoC phase option is changed to a fixed phase while an automatic phase-switch delay is active, _check_auto_phase_switch_delay() stops servicing the delay once template_phases is no longer 0, while Chargepoint.update() calls reset_phase_switch_delay() only when the chargemode or submode changes. Changing the minimum-SoC phase selection from automatic to a fixed option during a delay therefore leaves the charge point in PHASE_SWITCH_DELAY with its surplus reservation held; the requested phase switch never completes.

Cancel and release an active automatic phase-switch delay when the selected phase mode changes away from automatic.

Prompt for AI agents
In `packages/control/chargepoint/chargepoint.py` at line 667:

**Disabled automation strands phase delay**

When the automatic minimum-SoC phase option is changed to a fixed phase while an automatic phase-switch delay is active, `_check_auto_phase_switch_delay()` stops servicing the delay once `template_phases` is no longer 0, while `Chargepoint.update()` calls `reset_phase_switch_delay()` only when the chargemode or submode changes. Changing the minimum-SoC phase selection from automatic to a fixed option during a delay therefore leaves the charge point in `PHASE_SWITCH_DELAY` with its surplus reservation held; the requested phase switch never completes.

Cancel and release an active automatic phase-switch delay when the selected phase mode changes away from automatic.

if self.chargemode_changed:
self.data.control_parameter.failed_phase_switches = 0
message = message_ev if message_ev else message
Expand Down
3 changes: 3 additions & 0 deletions packages/control/chargepoint/control_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class ControlParameter:
phases: int = field(default=0, metadata={"topic": "control_parameter/phases"})
required_current: float = field(default=0, metadata={"topic": "control_parameter/required_current"})
required_currents: List[float] = field(default_factory=currents_list_factory)
# für auto_phase_switch(): die tatsächlich reservierte Leistung, damit das Zurücknehmen der Reservierung
# exakt zum Reservieren passt statt es (ggf. mit geänderten Werten) neu zu berechnen
reserved_power: float = field(default=0, metadata={"topic": "control_parameter/reserved_power"})
state: ChargepointState = field(default=ChargepointState.NO_CHARGING_ALLOWED,
metadata={"topic": "control_parameter/state"})
submode: Chargemode_enum = field(default=Chargemode_enum.STOP, metadata={"topic": "control_parameter/submode"})
Expand Down
25 changes: 14 additions & 11 deletions packages/control/ev/ev.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,8 @@ def auto_phase_switch(self,
delay = surplus_config.vehicle.phase_switch_delay * 60
if phases_in_use == 1:
direction_str = f"Umschaltung von 1 auf {max_phases}"
required_reserved_power = (control_parameter.min_current * max_phases * 230 -
self.ev_template.data.max_current_single_phase * 230)
required_reserved_power = max(0, control_parameter.min_current * max_phases * 230 -

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High · Use surplus above minimum

The change adds automatic phase switching, but does not make available PV surplus raise the charging current above the minimum-SoC current. Thus the requested lower-bound behavior—and the choice between that behavior and a fixed minimum-SoC current—is still missing.

Prompt for AI agents
In `packages/control/ev/ev.py` at line 324:

**Use surplus above minimum**

The change adds automatic phase switching, but does not make available PV surplus raise the charging current above the minimum-SoC current. Thus the requested lower-bound behavior—and the choice between that behavior and a fixed minimum-SoC current—is still missing.

From #4005:

Use available PV surplus during minimum-SoC charging while keeping the configured minimum-SoC current as a lower bound, with a choice to keep it fixed.

self.ev_template.data.max_current_single_phase * 230)
Comment on lines +324 to +325

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High · Minimum current is not preserved

The change enables automatic phase selection, but it still bases switching power on control_parameter.min_current, which is the vehicle's general minimum current rather than the configured AC/DC minimum-SoC current. Consequently, minimum-SoC charging does not get the requested lower-bound behavior, and a phase switch can use a current below that configured minimum; there is also no choice between fixed-current and lower-bound behavior.

Prompt for AI agents
In `packages/control/ev/ev.py` at lines 324-325:

**Minimum current is not preserved**

The change enables automatic phase selection, but it still bases switching power on `control_parameter.min_current`, which is the vehicle's general minimum current rather than the configured AC/DC minimum-SoC current. Consequently, minimum-SoC charging does not get the requested lower-bound behavior, and a phase switch can use a current below that configured minimum; there is also no choice between fixed-current and lower-bound behavior.

From #4005:

Make minimum-SoC charging optionally treat its configured current as a lower bound and use available PV surplus in addition to it.

Comment on lines +324 to +325

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High · Minimum-SoC current floor is lost

When minimum-SoC charging uses automatic phase selection and its configured current exceeds the vehicle’s general minimum, set_control_parameter() puts the vehicle’s general minimum in min_current, which auto_phase_switch() uses for the switch threshold and target current. The switch can therefore charge below the configured minimum-SoC current.

Use the configured minimum-SoC current for the automatic switch threshold and target during minimum-SoC charging.

Also at packages/modules/web_themes/koala/source/src/components/ChargePointPvSettings.vue:120, packages/modules/display_themes/cards/source/src/views/ChargePointsView.vue:1039, packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/ConfigPv.vue:82, packages/modules/web_themes/standard_legacy/web/index.html:959-960.

Prompt for AI agents
In `packages/control/ev/ev.py` at lines 324-325:

**Minimum-SoC current floor is lost**

When minimum-SoC charging uses automatic phase selection and its configured current exceeds the vehicle’s general minimum, `set_control_parameter()` puts the vehicle’s general minimum in `min_current`, which `auto_phase_switch()` uses for the switch threshold and target current. The switch can therefore charge below the configured minimum-SoC current.

Use the configured minimum-SoC current for the automatic switch threshold and target during minimum-SoC charging.

Also at `packages/modules/web_themes/koala/source/src/components/ChargePointPvSettings.vue:120`, `packages/modules/display_themes/cards/source/src/views/ChargePointsView.vue:1039`, `packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/ConfigPv.vue:82`, `packages/modules/web_themes/standard_legacy/web/index.html:959-960`.


new_phase = max_phases
new_current = control_parameter.min_current
Expand All @@ -336,9 +336,9 @@ def auto_phase_switch(self,
waiting_time = surplus_config.vehicle.switch_off_delay
all_surplus = data.data.counter_all_data.get_evu_counter().get_usable_surplus()
if control_parameter.state == ChargepointState.PHASE_SWITCH_DELAY:
# eigene reservierte Leistung addieren, wenn die Verzögerung für die Umschaltung läuft,
# da diese Leistung bereits für die Umschaltung reserviert ist.
surplus = all_surplus + max(0, required_reserved_power)
# mit der tatsächlich reservierten Leistung rechnen, nicht mit einer frisch berechneten - sonst
# können Reservieren und Zurücknehmen auseinanderlaufen, wenn sich zB max_phases währenddessen ändert
surplus = all_surplus + control_parameter.reserved_power
else:
surplus = all_surplus

Expand All @@ -358,8 +358,9 @@ def auto_phase_switch(self,
if condition:
# Wenn nach der Umschaltung weniger Leistung benötigt wird, soll während der Verzögerung keine
# neuen eingeschaltet werden.
control_parameter.reserved_power = required_reserved_power
data.data.counter_all_data.get_evu_counter(
).data.set.reserved_surplus += max(0, required_reserved_power)
).data.set.reserved_surplus += control_parameter.reserved_power
message = self.PHASE_SWITCH_DELAY_TEXT.format(
direction_str,
self._remaining_phase_switch_time(control_parameter,
Expand All @@ -384,14 +385,16 @@ def auto_phase_switch(self,
message = self.PHASE_SWITCH_DELAY_TEXT.format(direction_str, remaining_time)
else:
data.data.counter_all_data.get_evu_counter(
).data.set.reserved_surplus -= max(0, required_reserved_power)
).data.set.reserved_surplus -= control_parameter.reserved_power
control_parameter.reserved_power = 0
Comment on lines +388 to +389

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Restored reservations are not released

When a restart or upgrade restores a pending phase-switch delay created before reserved_power existed, reserved_power defaults to zero, so the timeout and reset paths subtract zero instead of the existing reservation from reserved_surplus. Usable surplus remains understated after the delay ends or is cancelled.

Provide a compatibility fallback or migration that restores the prior reservation amount before the timeout and reset paths release it.

Also at packages/control/ev/ev.py:447-449, packages/control/chargepoint/control_parameter.py:24.

Prompt for AI agents
In `packages/control/ev/ev.py` at lines 388-389:

**Restored reservations are not released**

When a restart or upgrade restores a pending phase-switch delay created before `reserved_power` existed, `reserved_power` defaults to zero, so the timeout and reset paths subtract zero instead of the existing reservation from `reserved_surplus`. Usable surplus remains understated after the delay ends or is cancelled.

Provide a compatibility fallback or migration that restores the prior reservation amount before the timeout and reset paths release it.

Also at `packages/control/ev/ev.py:447-449`, `packages/control/chargepoint/control_parameter.py:24`.

phases_to_use = new_phase
current = new_current
log.debug("Phasenumschaltung kann nun durchgeführt werden.")
control_parameter.state = ChargepointState.PHASE_SWITCH_AWAITED
else:
data.data.counter_all_data.get_evu_counter(
).data.set.reserved_surplus -= max(0, required_reserved_power)
).data.set.reserved_surplus -= control_parameter.reserved_power
control_parameter.reserved_power = 0
message = f"Verzögerung für die {direction_str} Phasen abgebrochen{condition_msg}"
control_parameter.state = ChargepointState.CHARGING_ALLOWED
control_parameter.timestamp_phase_switch_buffer_start = None
Expand Down Expand Up @@ -429,7 +432,7 @@ def _remaining_phase_switch_time(self, control_parameter: ControlParameter,
control_parameter.timestamp_phase_switch_buffer_start = None
return True, None

def reset_phase_switch_delay(self, control_parameter: ControlParameter, max_phases: int):
def reset_phase_switch_delay(self, control_parameter: ControlParameter):
""" Zurücksetzen der Zeitstempel und reservierten Leistung.

Die Phasenumschaltung kann nicht abgebrochen werden!
Expand All @@ -441,8 +444,8 @@ def reset_phase_switch_delay(self, control_parameter: ControlParameter, max_phas
# Wenn der Timer läuft, ist den Control-Parametern die alte Phasenzahl hinterlegt.
# bei der Umschaltung 3p1p wird keine Leistung reserviert
evu_counter = data.data.counter_all_data.get_evu_counter()
reserved = max(0, control_parameter.min_current * max_phases * 230 -
self.ev_template.data.max_current_single_phase * 230)
reserved = control_parameter.reserved_power
control_parameter.reserved_power = 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Reserved surplus remains unavailable

When a 1-to-3-phase delay has reserved power and the phase count changes from 1 before reset_phase_switch_delay() runs, reset_phase_switch_delay() releases and clears the reservation only when control_parameter.phases == 1, even though the charge-point update changes phases first. The reservation remains in reserved_surplus, reducing the surplus available to other charge points.

Release reserved_power and clear the delay reservation regardless of the current phase count.

Also at packages/control/chargepoint/chargepoint.py:191, packages/control/chargepoint/chargepoint.py:667, packages/control/ev/ev.py:449, packages/modules/display_themes/cards/source/src/views/ChargePointsView.vue:1039, packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/ConfigPv.vue:82, packages/modules/web_themes/koala/source/src/components/ChargePointPvSettings.vue:120, packages/modules/web_themes/standard_legacy/web/index.html:960.

Prompt for AI agents
In `packages/control/ev/ev.py` at line 448:

**Reserved surplus remains unavailable**

When a 1-to-3-phase delay has reserved power and the phase count changes from 1 before `reset_phase_switch_delay()` runs, `reset_phase_switch_delay()` releases and clears the reservation only when `control_parameter.phases == 1`, even though the charge-point update changes phases first. The reservation remains in `reserved_surplus`, reducing the surplus available to other charge points.

Release `reserved_power` and clear the delay reservation regardless of the current phase count.

Also at `packages/control/chargepoint/chargepoint.py:191`, `packages/control/chargepoint/chargepoint.py:667`, `packages/control/ev/ev.py:449`, `packages/modules/display_themes/cards/source/src/views/ChargePointsView.vue:1039`, `packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/ConfigPv.vue:82`, `packages/modules/web_themes/koala/source/src/components/ChargePointPvSettings.vue:120`, `packages/modules/web_themes/standard_legacy/web/index.html:960`.

evu_counter.data.set.reserved_surplus -= reserved
log.debug(f"Zurücksetzen von {reserved}W reservierter Leistung für die Phasenumschaltung. "
f"reservierte Leistung: {evu_counter.data.set.reserved_surplus} W")
Expand Down
60 changes: 60 additions & 0 deletions packages/control/ev/ev_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

from control import data

from control.chargepoint.chargepoint_state import ChargepointState
from control.chargepoint.control_parameter import ControlParameter
from control.ev.ev import Ev, get_ev_to_rfid
from control.limiting_value import LimitingValue, LoadmanagementLimit
from helpermodules import timecheck
from modules.common.abstract_vehicle import VehicleUpdateData
from modules.vehicles.mqtt.config import MqttSocSetup
Expand Down Expand Up @@ -77,6 +79,64 @@ def test_remaining_phase_switch_time(timestamp_last_phase_switch,
assert result == expected_result


@pytest.mark.parametrize(
"surplus, expected_condition",
[
pytest.param(6901, True, id="Überschuss reicht für min_current auf allen Phasen: umschalten"),
pytest.param(5000, False, id="Überschuss reicht nicht für min_current auf allen Phasen: nicht umschalten"),
],
)
def test_check_phase_switch_conditions_required_surplus_based_on_min_current(surplus, expected_condition):
""" required_surplus muss auf control_parameter.min_current basieren. """
# setup
ev = Ev(0)
ev.ev_template.data.max_phases = 3
ev.ev_template.data.max_current_single_phase = 16
ev.ev_template.data.nominal_difference = 1
control_parameter = ControlParameter()
control_parameter.min_current = 10
control_parameter.phases = 1
control_parameter.required_current = 6
limit = LoadmanagementLimit(message=None, limiting_value=LimitingValue.CURRENT)

# execution
condition, _ = ev._check_phase_switch_conditions(
control_parameter=control_parameter,
evse_current=0,
get_currents=[0, 0, 0],
get_power=0,
max_current_cp=32,
limit=limit,
surplus=surplus,
)

# evaluation: required_surplus = min_current(10) * max_phases(3) * 230 = 6900
assert condition == expected_condition


def test_reset_phase_switch_delay_uses_reserved_power_not_a_fresh_calculation(monkeypatch):
""" reset_phase_switch_delay() muss die beim Reservieren gespeicherte Leistung zurücknehmen, nicht neu
berechnen - sonst würde reserved_surplus driften, falls sich zB max_phases inzwischen geändert hat. """
# setup
ev = Ev(0)
ev.ev_template.data.max_current_single_phase = 16
control_parameter = ControlParameter()
control_parameter.state = ChargepointState.PHASE_SWITCH_DELAY
control_parameter.phases = 1
control_parameter.reserved_power = 500
evu_counter = Mock()
evu_counter.data.set.reserved_surplus = 1000
monkeypatch.setattr(data.data.counter_all_data, "get_evu_counter", Mock(return_value=evu_counter))

# execution
ev.reset_phase_switch_delay(control_parameter)

# evaluation
assert evu_counter.data.set.reserved_surplus == 500
assert control_parameter.reserved_power == 0
assert control_parameter.state == ChargepointState.CHARGING_ALLOWED


@pytest.fixture(autouse=True)
def data_module() -> None:
data.data_init(Mock())
Expand Down
1 change: 1 addition & 0 deletions packages/helpermodules/setdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ def process_control_parameter_topics(self, msg):
elif "/control_parameter/current_plan" in msg.topic:
self._validate_value(msg, int)
elif ("/control_parameter/min_current" in msg.topic or
"/control_parameter/reserved_power" in msg.topic or
"/control_parameter/timestamp_switch_on_off" in msg.topic or
"/control_parameter/timestamp_charge_start" in msg.topic or
"/control_parameter/timestamp_chargemode_changed" in msg.topic or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,28 @@ export default {
>
Maximum
</i-button>
<i-button
:color="
mqttStore.getChargePointConnectedVehiclePvChargingMinSocPhases(
modalChargePointId,
) == 0
? 'primary'
: ''
"
:active="
mqttStore.getChargePointConnectedVehiclePvChargingMinSocPhases(
modalChargePointId,
) == 0
"
@click="
setChargePointConnectedVehiclePvChargingMinSocPhases(
modalChargePointId,
0,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Automatik selection is ignored

When a user selects Automatik in the Cards theme, the selected_phases && guard rejects the selected value 0, so setChargePointConnectedVehiclePvChargingMinSocPhases() does not update the saved phase selection.

Change the guard to accept 0 as a valid selection.

Prompt for AI agents
In `packages/modules/display_themes/cards/source/src/views/ChargePointsView.vue` at line 1036:

**Automatik selection is ignored**

When a user selects Automatik in the Cards theme, the `selected_phases &&` guard rejects the selected value `0`, so `setChargePointConnectedVehiclePvChargingMinSocPhases()` does not update the saved phase selection.

Change the guard to accept `0` as a valid selection.

"
>
Automatik

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High · Automatic option is absent in served themes

When the PR is deployed using the checked-in web assets without a separate frontend build, the deployed pages load the prebuilt files under web/, not these edited source components, so without rebuilding and including those files the new buttons do not appear to users.

Rebuild and include the generated web assets for each theme whose source changes in this PR.

Also at packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/ConfigPv.vue:82, packages/modules/web_themes/koala/source/src/components/ChargePointPvSettings.vue:120.

Prompt for AI agents
In `packages/modules/display_themes/cards/source/src/views/ChargePointsView.vue` at line 1039:

**Automatic option is absent in served themes**

When the PR is deployed using the checked-in web assets without a separate frontend build, the deployed pages load the prebuilt files under `web/`, not these edited source components, so without rebuilding and including those files the new buttons do not appear to users.

Rebuild and include the generated web assets for each theme whose source changes in this PR.

Also at `packages/modules/web_themes/colors/source/src/components/chargePointList/cpConfig/ConfigPv.vue:82`, `packages/modules/web_themes/koala/source/src/components/ChargePointPvSettings.vue:120`.

</i-button>
</i-button-group>
</i-form-group>
</i-form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@
<RadioInput2
v-model="cp.pvMinSocPhases"
class="grid-col-1"
:columns="2"
:options="[
['1', 1],
['Maximum', 3],
['Auto', 0],
]"
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ const phaseOptions = [
const phaseOptionsMinSoc = [
{ value: 1, label: '1' },
{ value: 3, label: 'Maximum' },
{ value: 0, label: 'Automatik' },
];

const pvMinCurrent = computed(() =>
Expand Down
3 changes: 3 additions & 0 deletions packages/modules/web_themes/standard_legacy/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,9 @@ <h3>Einstellungen für "PV"</h3>
<label class="btn btn-outline-info btn-toggle">
<input type="radio" name="phasesCp" data-option="3"> Maximum
</label>
<label class="btn btn-outline-info btn-toggle">
<input type="radio" name="phasesCp" data-option="0"> Automatik
</label>
</div>
</div>
</div>
Expand Down
Loading