Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion packages/control/algorithm/additional_current.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def set_additional_current(self) -> None:
available_currents, limit = Loadmanagement().get_available_currents(missing_currents, counter, cp)
log.debug(f"cp {cp.num} available currents {available_currents} missing currents "
f"{missing_currents} limit {limit.message}")
cp.data.control_parameter.limit = limit
if limit.limiting_value is not None:
cp.data.control_parameter.limit = limit
Comment on lines +33 to +34
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Same issue as in min_current.py: skipping the assignment when limit.limiting_value is None means cp.data.control_parameter.limit can remain set to an old limiting reason even after the constraint is gone. That stale state can propagate into phase-switch logic and status messaging. Consider clearing/resetting the limit explicitly when there is no active limitation.

Suggested change
if limit.limiting_value is not None:
cp.data.control_parameter.limit = limit
cp.data.control_parameter.limit = limit

Copilot uses AI. Check for mistakes.
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)
Expand Down
3 changes: 2 additions & 1 deletion packages/control/algorithm/min_current.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def set_min_current(self) -> None:
if max(missing_currents) > 0:
available_currents, limit = Loadmanagement().get_available_currents(
missing_currents, counter, cp)
cp.data.control_parameter.limit = limit
if limit.limiting_value is not None:
cp.data.control_parameter.limit = limit
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

With this change, when Loadmanagement().get_available_currents(...) returns a limit with limiting_value is None, the chargepoint's control_parameter.limit is no longer updated/cleared and will retain whatever value was set in a previous iteration. That can lead to stale limit state being displayed and can affect downstream logic that reads control_parameter.limit. If you only want to skip publishing limit messages, consider still assigning a neutral/empty LoadmanagementLimit here to clear the previous limit.

Suggested change
cp.data.control_parameter.limit = limit
cp.data.control_parameter.limit = limit
else:
cp.data.control_parameter.limit = None

Copilot uses AI. Check for mistakes.
available_for_cp = common.available_current_for_cp(
cp, counts, available_currents, missing_currents)
current = common.get_current_to_set(
Expand Down
4 changes: 3 additions & 1 deletion packages/control/algorithm/surplus_controlled.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ def _set(self,
cp,
feed_in=feed_in_yield
)
cp.data.control_parameter.limit = limit
# im PV-Laden wird der Strom immer durch die Leistung begrenzt
if limit.limiting_value is not None and limit.limiting_value != LimitingValue.POWER:
cp.data.control_parameter.limit = limit
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

This conditional assignment can leave cp.data.control_parameter.limit stale when (a) there is no limiting (limit.limiting_value is None) or (b) limiting is by POWER (which is skipped). Since cp.data.control_parameter.limit is later consumed by algorithm.py for auto_phase_switch, an old CURRENT/UNBALANCED limit may incorrectly influence phase-switch decisions and UI state. If the intention is to ignore POWER limits in PV mode, consider explicitly clearing/resetting control_parameter.limit when the current limit is POWER or None, rather than leaving the previous value in place.

Suggested change
cp.data.control_parameter.limit = limit
cp.data.control_parameter.limit = limit
else:
# Clear any stale CURRENT/UNBALANCED limit when there is no limiting
# or when limiting is by POWER (ignored in PV mode).
cp.data.control_parameter.limit = None

Copilot uses AI. Check for mistakes.
available_for_cp = common.available_current_for_cp(cp, counts, available_currents, missing_currents)
if counter.get_control_range_state(feed_in_yield) == ControlRangeState.MIDDLE:
pv_charging = data.data.general_data.data.chargemode_config.pv_charging
Expand Down
29 changes: 20 additions & 9 deletions packages/control/auto_phase_switch_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from control.chargepoint.control_parameter import ControlParameter
from control.counter import Counter, CounterData, Set

from control.limiting_value import LoadmanagementLimit
from control.limiting_value import LimitingValue, LoadmanagementLimit
from control.ev.charge_template import ChargeTemplate
from control.pv_all import PvAll
from control.bat_all import BatAll
Expand Down Expand Up @@ -153,21 +153,32 @@ def test_auto_phase_switch(monkeypatch, vehicle: Ev, params: Params):


@pytest.mark.parametrize(
"evse_current, get_currents, all_surplus, expected",
"evse_current, get_currents, all_surplus, limit, expected",
[
pytest.param(8, [7.7]*3, 100, (False, Ev.ENOUGH_POWER), id="kein 1p3p, genug Leistung für mehrphasige Ladung"),
pytest.param(10, [9.8, 0, 0], 50, (False, Ev.NOT_ENOUGH_POWER),
pytest.param(8, [7.7]*3, 100, LoadmanagementLimit(None, None), (False, Ev.ENOUGH_POWER),
id="kein 1p3p, genug Leistung für mehrphasige Ladung"),
pytest.param(10, [9.8, 0, 0], 50, LoadmanagementLimit(None, None), (False, Ev.NOT_ENOUGH_POWER),
id="kein 1p3p, nicht genug Leistung, um auf 3p zu schalten"),
pytest.param(16, [14, 0, 0], 5000, (False, Ev.CURRENT_OUT_OF_NOMINAL_DIFFERENCE),
pytest.param(16, [14, 0, 0], 5000, LoadmanagementLimit(None, None),
(False, Ev.CURRENT_OUT_OF_NOMINAL_DIFFERENCE),
id="kein 1p3p, Auto lädt nicht mit vorgegebener Maximalstromstärke"),
pytest.param(6, [7.5]*3, -20, (False, Ev.CURRENT_OUT_OF_NOMINAL_DIFFERENCE),
pytest.param(6, [7.5]*3, -20, LoadmanagementLimit(None, None), (False, Ev.CURRENT_OUT_OF_NOMINAL_DIFFERENCE),
id="kein 1p3p, Auto lädt nicht mit vorgegebener Minimalstromstärke"),
pytest.param(16, [15.8, 0, 0], 5000, (True, None), id="1p3p"),
pytest.param(6, [5.8]*3, -10, (True, None), id="3p1p"),
pytest.param(16, [15.8, 0, 0], 5000, LoadmanagementLimit(None, None), (True, None), id="1p3p"),
pytest.param(6, [5.8]*3, -10, LoadmanagementLimit(None, None), (True, None), id="3p1p"),
pytest.param(10, [9.8, 0, 0], 5000,
LoadmanagementLimit(message=", da der Maximal-Strom an Zähler Test erreicht ist.",
limiting_value=LimitingValue.CURRENT), (True, None),
id="1p3p, da durch die Begrenzung des LM nicht mit maximalem Strom geladen wird"),
pytest.param(10, [9.8, 0, 0], 5000,
LoadmanagementLimit(message=", da die maximale Schieflast an Zähler Test erreicht ist.",
limiting_value=LimitingValue.UNBALANCED_LOAD), (True, None),
id="1p3p, da durch die Begrenzung der Schieflast nicht mit maximalem Strom geladen wird"),
])
def test_check_phase_switch_conditions(evse_current: int,
get_currents: List[float],
all_surplus: int,
limit: LoadmanagementLimit,
expected: Tuple[bool, Optional[str]],
monkeypatch):
# setup
Expand All @@ -183,7 +194,7 @@ def test_check_phase_switch_conditions(evse_current: int,
get_currents,
sum(get_currents)*230,
16,
LoadmanagementLimit(None, None))
limit)

# evaluation
assert (phase_switch, condition_msg) == expected
4 changes: 4 additions & 0 deletions packages/control/chargepoint/chargepoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from control.ev.ev import Ev
from control import phase_switch
from control.chargepoint.chargepoint_state import CHARGING_STATES, ChargepointState
from control.limiting_value import loadmanagement_limit_factory
from control.text import BidiState
from helpermodules.phase_handling import convert_single_evu_phase_to_cp_phase
from helpermodules.pub import Pub
Expand Down Expand Up @@ -266,6 +267,9 @@ def reset_control_parameter_at_charge_stop(self) -> None:
control_parameter = control_parameter_factory()
self.data.control_parameter = control_parameter

def reset_values_before_algorithm(self) -> None:
self.data.control_parameter.limit = loadmanagement_limit_factory()

def initiate_control_pilot_interruption(self):
""" prüft, ob eine Control Pilot- Unterbrechung erforderlich ist und führt diese durch.
"""
Expand Down
6 changes: 4 additions & 2 deletions packages/control/ev/ev.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,10 @@ def _check_phase_switch_conditions(self,
all_surplus = data.data.counter_all_data.get_evu_counter().get_usable_surplus(feed_in_yield)
required_surplus = control_parameter.min_current * max_phases_ev * 230 - get_power
unbalanced_load_limit_reached = limit.limiting_value == LimitingValue.UNBALANCED_LOAD
condition_1_to_3 = (((get_medium_charging_current(get_currents) > max_current_range and
all_surplus > required_surplus) or unbalanced_load_limit_reached) and
current_limit_reached = (limit.limiting_value == LimitingValue.CURRENT or
limit.limiting_value == LimitingValue.CURRENT.value)
condition_1_to_3 = ((((get_medium_charging_current(get_currents) > max_current_range or current_limit_reached)
and all_surplus > required_surplus) or unbalanced_load_limit_reached) and
phases_in_use == 1)
condition_3_to_1 = get_medium_charging_current(
get_currents) < min_current_range and all_surplus <= 0 and phases_in_use > 1
Expand Down
2 changes: 2 additions & 0 deletions packages/control/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ def setup_algorithm(self) -> None:
data.data.cp_all_data.no_charge()
data.data.counter_all_data.set_home_consumption()
data.data.io_actions.setup()
for cp in data.data.cp_data.values():
cp.reset_values_before_algorithm()
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

Durch das Zurücksetzen von cp.data.control_parameter.limit direkt vor control.calc_current() geht der Loadmanagement-Limit-Status aus dem vorherigen Zyklus verloren. Da Algorithm._check_auto_phase_switch_delay() (und damit Ev.auto_phase_switch) zu Beginn des Algorithmus ausgeführt wird, sieht die Phasenumschaltung in diesem Zyklus immer limit=None und kann die neuen Umschaltbedingungen (z.B. CURRENT/UNBALANCED_LOAD) nicht mehr berücksichtigen. Lösung: limit nicht in Prepare.setup_algorithm() zurücksetzen, sondern stattdessen im Algorithmus nach der Phasenumschaltungsprüfung explizit aktualisieren (z.B. immer cp.data.control_parameter.limit = limit setzen, auch bei None) oder ein separates Feld für „aktuelles Limit dieses Zyklus“ vs. „letztes Limit“ verwenden.

Suggested change
for cp in data.data.cp_data.values():
cp.reset_values_before_algorithm()
for cp in data.data.cp_data.values():
# Limit-Wert des vorherigen Zyklus sichern, damit er für die
# Phasenumschaltung im Algorithmus weiterhin verfügbar bleibt.
prev_limit = None
try:
prev_limit = getattr(
getattr(cp, "data", None),
"control_parameter",
None,
)
if prev_limit is not None:
prev_limit = getattr(prev_limit, "limit", None)
except AttributeError:
prev_limit = None
cp.reset_values_before_algorithm()
# Gesicherten Limit-Wert wiederherstellen.
try:
if hasattr(cp, "data") and hasattr(cp.data, "control_parameter"):
cp.data.control_parameter.limit = prev_limit
except AttributeError:
pass

Copilot uses AI. Check for mistakes.
except Exception:
log.exception("Fehler im Prepare-Modul")
data.data.print_all()
Loading