Skip to content

[BUG]Phasenumschaltung Series2 seit #3326: CP wird ohne Wartezeit unter Last getrennt (EV meldet Ladefehler), serieller Modbus-Aussetzer bricht die Umschaltung ab #4289

Open

Description

@Stefan3934

Umgebung

  • openWB series2 mit interner Phasenumschaltung (EVSE DIN an /dev/ttyUSB0, Modbus-ID 1)
  • Release 2.3.0 (8c58e4b84), Betrieb als "primary" mit internem Ladepunkt (LP3)
  • Fahrzeug: Audi A6 e-tron (2025, PPE-Plattform), 1-/3-phasig, Lademodus PV mit automatischer Phasenumschaltung
  • Unter 2.2.1 (82510a223) lief die Phasenumschaltung mit demselben Fahrzeug über Monate ohne Auffälligkeiten.

Beschreibung

Seit dem Update auf 2.3.0 scheitert die Phasenumschaltung regelmäßig, mit zwei verschiedenen Symptomen:

  1. Fahrzeug meldet "Ladenetz Fehler" und nimmt die Ladung nicht wieder auf. Die Wallbox zeigt danach dauerhaft
    "Strom freigegeben, warten auf Fahrzeug". Nach Ablauf der Wartezeit löst 2.3.0 eine CP-Unterbrechung aus
    (neue Logik "Auto lädt trotz Freigabe nicht"), die ebenfalls nichts bringt. Erst Ent- und Verriegeln des
    Fahrzeugs setzt den Fehler im Auto zurück.
  2. Umschaltung bricht mit Modbus-Fehler ab, Phasenzahl bleibt falsch. perform_phase_switch wirft
    Modbus Error: [Invalid Message] No response received beim Rücklesen der EVSE, die GPIO-Aktion findet nicht
    statt, das Relais bleibt auf der alten Phasenzahl, die Regelung gibt mit der neuen Soll-Phasenzahl wieder
    Strom frei. Ladung kommt nicht zustande.

Beide Symptome lassen sich auf 1e41b2080 (#3326, "EVSE-Ruecklesen vor Phasenumschaltung") zurückführen,
siehe Abschnitt Ursache.

Ablauf am 06.11.2026 (Log-Auszüge)

Die Logs lagen bis 13:14 auf WARNING, daher ist für die erste Umschaltung nur der Zeitstempel und das
Verhalten dokumentiert; ab 13:14:05 DEBUG.

13:10:34 Umschaltung 1 → 3 Phasen (Wechsel PV → Sofortladen, Auto lud 1-phasig mit ca. 3,4 kW):

  • timestamp_last_phase_switch = 1793967034 (= 13:10:34)
  • Auto zeigt sofort "Ladenetz Fehler", Wallbox "Strom freigegeben, warten auf Fahrzeug", keine Ladung.
  • 13:14:05 greift die neue CP-Unterbrechungs-Logik von 2.3.0:
2026-11-06 13:14:05,658 - {modules.internal_chargepoint_handler.internal_chargepoint_handler:44} - {DEBUG:Internal Chargepoint} - Values from parentWB: InternalChargepointData(cp_interruption_duration=5, parent_cp='3', phases_to_use=3, ...
2026-11-06 13:14:05,784 - {modules.internal_chargepoint_handler.internal_chargepoint_handler:80} - {DEBUG:Internal Chargepoint} - Thread zur CP-Unterbrechung an LP0 gestartet.
2026-11-06 13:14:07,113 - {modules.common.evse:57} - {DEBUG:Internal Chargepoint} - Gesetzte Stromstärke EVSE: 0, Status: 1, Modbus-ID: 1
2026-11-06 13:14:13,517 - {modules.common.evse:57} - {DEBUG:Internal Chargepoint} - Gesetzte Stromstärke EVSE: 600, Status: 2, Modbus-ID: 1

Das Auto bleibt in Status 2 (angesteckt, lädt nicht). Daraufhin Wechsel zurück auf PV (1-phasig):

2026-11-06 13:14:24,947 - {control.chargepoint.chargepoint:74} - {INFO:MainThread} - LP 3: Die Ladung wird gestartet, sobald in 29 Sek. die Wartezeit abgelaufen ist.
2026-11-06 13:14:34,150 - {control.chargepoint.chargepoint:74} - {INFO:MainThread} - LP 3: Die Ladung wird gestartet, sobald in 20 Sek. die Wartezeit abgelaufen ist.
2026-11-06 13:14:45,398 - {control.chargepoint.chargepoint:74} - {INFO:MainThread} - LP 3: Die Ladung wird gestartet, sobald in 9 Sek. die Wartezeit abgelaufen ist.

13:15:07 Umschaltung 3 → 1 Phasen, Abbruch durch Modbus-Fehler:

2026-11-06 13:15:07,031 - {control.phase_switch:17} - {DEBUG:MainThread} - Starte Thread zur Phasenumschaltung an LP3
2026-11-06 13:15:07,036 - {control.chargepoint.chargepoint:432} - {DEBUG:MainThread} - start phase switch phases_to_use 3 control_parameter phases 1
2026-11-06 13:15:09,820 - {modules.internal_chargepoint_handler.internal_chargepoint_handler:44} - {DEBUG:Internal Chargepoint} - Values from parentWB: InternalChargepointData(cp_interruption_duration=0, parent_cp='3', phases_to_use=1, set_current=0, trigger_phase_switch=True)
2026-11-06 13:15:09,966 - {modules.internal_chargepoint_handler.internal_chargepoint_handler:59} - {DEBUG:Internal Chargepoint} - Switch Phases from 0 to 1
2026-11-06 13:15:09,978 - {modules.internal_chargepoint_handler.internal_chargepoint_handler:73} - {DEBUG:Internal Chargepoint} - Thread zur Phasenumschaltung an LP0 gestartet.
2026-11-06 13:15:11,198 - {modules.common.modbus:91} - {DEBUG:perform phase switch 0} - Close Modbus TCP connection
2026-11-06 13:15:11,209 - {modules.common.fault_state:47} - {ERROR:perform phase switch 0} - Ladepunkt 0: FaultState FaultStateLevel.ERROR, FaultStr modules.common.modbus <class 'Exception'> modules.common.modbus Modbus Error: [Input/Output] Modbus Error: [Invalid Message] No response received, expected at least 2 bytes (0 received), Traceback:
Traceback (most recent call last):
  File "/var/www/html/openWB/packages/modules/common/modbus.py", line 121, in __read_registers
    raise Exception(__name__+" "+str(response))
Exception: modules.common.modbus Modbus Error: [Input/Output] Modbus Error: [Invalid Message] No response received, expected at least 2 bytes (0 received)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/var/www/html/openWB/packages/modules/internal_chargepoint_handler/chargepoint_module.py", line 150, in perform_phase_switch
    _, _, evse_current = evse.get_plug_charge_state()
  File "/var/www/html/openWB/packages/modules/common/evse.py", line 53, in get_plug_charge_state
    raw_set_current, _, state_number = self.client.read_holding_registers(
  File "/var/www/html/openWB/packages/modules/common/modbus.py", line 153, in read_holding_registers
    return self.__read_registers(
  File "/var/www/html/openWB/packages/modules/common/modbus.py", line 136, in __read_registers
    raise Exception(__name__+" "+str(type(e))+" " + str(e)) from e
Exception: modules.common.modbus <class 'Exception'> modules.common.modbus Modbus Error: [Input/Output] Modbus Error: [Invalid Message] No response received, expected at least 2 bytes (0 received)

Danach: keine GPIO-Aktion, Relais bleibt auf 3 Phasen, Soll-Phasenzahl 1, Regelung gibt 6 A frei, Auto bleibt
im Fehlerzustand:

2026-11-06 13:15:13,363 - ... get=Get(charge_state=False, currents=[0.0, 0.0, 0.0], evse_current=0.0, ... fault_str="modules.common.modbus <class 'Exc...
2026-11-06 13:15:18,066 - {modules.common.evse:57} - {DEBUG:Internal Chargepoint} - Gesetzte Stromstärke EVSE: 600, Status: 2, Modbus-ID: 1
2026-11-06 13:15:15,990 - {helpermodules.changed_values_handler:138} - {DEBUG:MainThread} - Topic openWB/set/chargepoint/3/get/state_str, Payload Strom freigegeben, warten auf Fahrzeug. ...
2026-11-06 13:15:25,129 - ... Payload Strom freigegeben, warten auf Fahrzeug. ...
2026-11-06 13:15:35,462 - ... Payload Strom freigegeben, warten auf Fahrzeug. ...
(wiederholt sich alle 10 s)

Ursache

Vergleich perform_phase_switch in packages/modules/internal_chargepoint_handler/chargepoint_module.py:

2.2.1 (82510a223):

with SingleComponentUpdateContext(self.fault_state, update_always=False):
    self._client.evse_client.set_current(0)
time.sleep(5)
GPIO.output(gpio_cp, GPIO.HIGH)  # CP off

2.3.0 (8c58e4b84, seit 1e41b2080 / #3326):

with SingleComponentUpdateContext(self.fault_state, update_always=False, reraise=True):
    evse.set_current(0)
    for _ in range(20):  # poll up to 10s (20 × 0.5s) for EVSE to confirm 0 A
        _, _, evse_current = evse.get_plug_charge_state()
        if evse_current == 0:
            break
        time.sleep(0.5)
        evse.set_current(0)
    else:
        raise Exception("Ladung konnte nicht gestoppt werden - Phasenumschaltung abgebrochen.")
GPIO.output(gpio_cp, GPIO.HIGH)  # CP off

Zwei Probleme:

  1. Die Wartezeit zwischen 0 A und CP-Trennung ist faktisch entfallen. get_plug_charge_state() liefert den
    EVSE-Sollwert (Register 1000), nicht den Iststrom. Der steht unmittelbar nach set_current(0) auf 0, die
    Schleife bricht beim ersten Durchlauf ab, und CP wird innerhalb von ca. 0,3 s nach dem 0-A-Befehl getrennt.
    Das Fahrzeug hat zu diesem Zeitpunkt die Ladung noch nicht beendet und die Schütze noch nicht geöffnet.
    Nach IEC 61851 darf das EV bis zu 3 s brauchen, um auf eine PWM-Änderung zu reagieren. In 2.2.1 deckten die
    festen 5 s das ab. Fahrzeuge, die einen CP-Abriss unter Last tolerieren, zeigen nur einen Neustart der
    Ladung; der Audi (PPE) geht in einen Ladefehler, der nur durch Ent-/Verriegeln zurückgesetzt wird.
  2. Jeder serielle Modbus-Fehler beim Rücklesen bricht die Umschaltung ab (reraise=True). Die Rückleseschleife
    erzeugt bis zu 40 zusätzliche Transaktionen auf dem RS485-Bus genau im Umschaltmoment. Ein einzelner Aussetzer
    ("No response received", bei uns sporadisch auch an anderen Stellen zu sehen) reicht, und die Exception
    verlässt perform_phase_switch vor der GPIO-Sequenz. Die Hardware bleibt auf der alten Phasenzahl, die
    Regelung arbeitet mit der neuen. Bis zum nächsten Umschaltversuch kommt keine Ladung zustande.
    CP nach Fehler/Abbruch bei Phasenumschaltung und CP-Unterbrechung immer wieder verbinden #4037 behandelt die Folge (CP bleibt bei Exception nach GPIO.HIGH getrennt), nicht diesen Abbruch davor.

Vorschlag

Das Rücklesen nur als Best-Effort behandeln, die feste Wartezeit aus 2.2.1 beibehalten und die GPIO-Sequenz
immer ausführen (CP im finally wieder verbinden, wie in #4037). Läuft bei uns so seit 06.11.2026:

PHASE_SWITCH_SETTLE_SECONDS = 5

def perform_phase_switch(self, phases_to_use: int) -> None:
    gpio_cp, gpio_relay = self._client.get_pins_phase_switch(phases_to_use)
    evse = self._client.evse_client
    confirmed = False
    try:
        evse.set_current(0)
        for _ in range(10):  # bis zu 5 s auf Bestätigung 0 A warten
            try:
                _, _, evse_current = evse.get_plug_charge_state()
                if evse_current == 0:
                    confirmed = True
                    break
            except Exception as e:
                log.warning(f"Phasenumschaltung LP{self.local_charge_point_num}: EVSE-Rücklesen "
                            f"fehlgeschlagen ({e}), weiter.")
            time.sleep(0.5)
            try:
                evse.set_current(0)
            except Exception as e:
                log.warning(f"Phasenumschaltung LP{self.local_charge_point_num}: 0 A setzen "
                            f"fehlgeschlagen ({e}), weiter.")
    except Exception as e:
        log.warning(f"Phasenumschaltung LP{self.local_charge_point_num}: 0 A setzen "
                    f"fehlgeschlagen ({e}), Umschaltung wird trotzdem durchgeführt.")
    if not confirmed:
        log.warning(f"Phasenumschaltung LP{self.local_charge_point_num}: EVSE hat 0 A nicht bestätigt, "
                    f"Umschaltung wird nach Wartezeit trotzdem durchgeführt.")
    # Immer die feste Wartezeit einhalten, damit das EV die Ladung sauber beendet hat.
    time.sleep(self.PHASE_SWITCH_SETTLE_SECONDS)
    try:
        GPIO.output(gpio_cp, GPIO.HIGH)  # CP off
        GPIO.output(gpio_relay, GPIO.HIGH)  # 3 on/off
        time.sleep(5)
        GPIO.output(gpio_relay, GPIO.LOW)  # 3 on/off
        time.sleep(5)
    finally:
        GPIO.output(gpio_cp, GPIO.LOW)  # CP on - in jedem Fall wieder verbinden
    time.sleep(1)
    self.old_phases_in_use = phases_to_use

Verifikation 13:25:28 (3 → 1 Phasen, mit obigem Code, Auto lud vorher 3-phasig):

2026-11-06 13:25:28,053 - {modules.internal_chargepoint_handler.internal_chargepoint_handler:59} - {DEBUG:Internal Chargepoint} - Switch Phases from 0 to 1
2026-11-06 13:25:28,059 - {modules.internal_chargepoint_handler.internal_chargepoint_handler:73} - {DEBUG:Internal Chargepoint} - Thread zur Phasenumschaltung an LP0 gestartet.
2026-11-06 13:25:28,341 - {modules.common.evse:57} - {DEBUG:perform phase switch 0} - Gesetzte Stromstärke EVSE: 0, Status: 2, Modbus-ID: 1
2026-11-06 13:25:31,394 - {modules.common.evse:57} - {DEBUG:Internal Chargepoint} - Gesetzte Stromstärke EVSE: 0, Status: 2, Modbus-ID: 1   <- 5 s Wartezeit, CP noch verbunden
2026-11-06 13:25:33,489 - {modules.common.evse:57} - {DEBUG:Internal Chargepoint} - Gesetzte Stromstärke EVSE: 0, Status: 1, Modbus-ID: 1   <- CP getrennt
2026-11-06 13:25:43,141 - {modules.common.evse:57} - {DEBUG:Internal Chargepoint} - Gesetzte Stromstärke EVSE: 0, Status: 1, Modbus-ID: 1
2026-11-06 13:25:45,184 - {modules.common.evse:57} - {DEBUG:Internal Chargepoint} - Gesetzte Stromstärke EVSE: 0, Status: 2, Modbus-ID: 1   <- CP wieder verbunden
2026-11-06 13:25:47,435 - {modules.common.evse:57} - {DEBUG:Internal Chargepoint} - Gesetzte Stromstärke EVSE: 600, Status: 2, Modbus-ID: 1
2026-11-06 13:25:53,977 - {modules.common.evse:57} - {DEBUG:Internal Chargepoint} - Gesetzte Stromstärke EVSE: 600, Status: 3, Modbus-ID: 1 <- Auto lädt
2026-11-06 13:26:09,408 - {modules.common.evse:57} - {DEBUG:Internal Chargepoint} - Gesetzte Stromstärke EVSE: 1372, Status: 3, Modbus-ID: 1

Umschaltung ohne Fehler, Auto nimmt die Ladung 1-phasig mit 13,7 A wieder auf, keine Warnung aus
perform_phase_switch.

Offene Frage an die Maintainer: Wenn das Rücklesen den Iststrom bestätigen soll, müsste es den Zählerstrom
(counter_state.currents) oder den EVSE-Status (3 → 2) abwarten, nicht den Sollwert. Die feste Wartezeit ist
der einfachere und bisher bewährte Weg.

Verwandte PRs/Issues

Activity

  1. kaindl commented on Nov 10, 2026

    @kaindl
    Contributor

    Sollte es nicht reichen, den charging-State von get_plug_charge_state mit abzufragen? Der prüft doch schon auf Strom > 0, oder?

    Also von

    with SingleComponentUpdateContext(self.fault_state, update_always=False, reraise=True):
        evse.set_current(0)
        for _ in range(20):  # poll up to 10s (20 × 0.5s) for EVSE to confirm 0 A
            _, _, evse_current = evse.get_plug_charge_state()
            if evse_current == 0:
                break
            time.sleep(0.5)
            evse.set_current(0)
        else:
            raise Exception("Ladung konnte nicht gestoppt werden - Phasenumschaltung abgebrochen.")
    GPIO.output(gpio_cp, GPIO.HIGH)  # CP off
    

    zu

    with SingleComponentUpdateContext(self.fault_state, update_always=False, reraise=True):
        evse.set_current(0)
        for _ in range(20):  # poll up to 10s (20 × 0.5s) for EVSE to confirm 0 A
            _, charging, evse_current = evse.get_plug_charge_state()
            if evse_current == 0 and not charging:
                break
            time.sleep(0.5)
            evse.set_current(0)
        else:
            raise Exception("Ladung konnte nicht gestoppt werden - Phasenumschaltung abgebrochen.")
    GPIO.output(gpio_cp, GPIO.HIGH)  # CP off```
    
  2. Stefan3934 commented on Nov 11, 2026

    @Stefan3934
    Author

    Danke für den Blick drauf. charging zusätzlich abzufragen ändert leider nichts, denn charging wird in
    get_plug_charge_state() aus demselben Sollwert abgeleitet:

    # modules/common/evse.py
    raw_set_current, _, state_number = self.client.read_holding_registers(1000, [ModbusDataType.UINT_16]*3, unit=self.id)
    self.evse_current = int(raw_set_current)
    state = EvseStatusCode(state_number)
    charging = self.evse_current > 0 if state.charge_enabled else False

    Unmittelbar nach set_current(0) liest Register 1000 bereits 0, damit ist charging False, egal ob die EVSE
    noch Status 3 (C, Fahrzeug lädt) meldet. evse_current == 0 and not charging ist also dieselbe Bedingung wie
    evse_current == 0 und bricht die Schleife genauso im ersten Durchlauf ab.

    Was tatsächlich den Zustand des Fahrzeugs abbildet, ist state_number (Register 1002): 3/4 = Fahrzeug lädt
    (State C), 2 = angesteckt, lädt nicht (State B). Darauf müsste die Schleife warten, nicht auf den Sollwert.
    get_plug_charge_state() gibt den Status nicht zurück, man bräuchte also entweder den Rohstatus im Rückgabewert
    oder einen eigenen Lesezugriff in perform_phase_switch.

    In meinen Logs vom 06.11. sieht man beide Fälle:

    • 13:10:34, Umschaltung 1→3 unter Last (Auto lud mit 3,4 kW): Sollwert sofort 0, CP wurde nach ~0,3 s
      getrennt, Audi meldet "Ladenetz Fehler". Mit der alten festen Pause (time.sleep(5) in 2.2.1, siehe auch
      den Kommentar in control/phase_switch.py: "Wenn gerade geladen wird, muss vor der Umschaltung eine Pause
      von 5s gemacht werden") trat das nie auf.
    • 13:15:09, Umschaltung 3→1: get_plug_charge_state() wirft nach einem seriellen Aussetzer
      (No response received), wegen reraise=True bricht die Umschaltung vor der GPIO-Sequenz ab, Relais bleibt
      auf 3 Phasen, Soll-Phasenzahl 1, die Regelung gibt Strom frei und wartet vergeblich.

    Der zweite Punkt bleibt auch mit einem korrekten Statuskriterium bestehen: ein einzelner Modbus-Fehler im
    Rücklesen darf die Umschaltung nicht abbrechen. Mein Vorschlag wäre daher:

    1. In der Schleife auf state_number not in (3, 4) warten (Fahrzeug hat die Ladung beendet), nicht auf den
      Sollwert.
    2. Exceptions beim Rücklesen innerhalb der Schleife abfangen und weiter pollen, statt die Umschaltung zu
      verlassen.
    3. Bleibt das Fahrzeug nach 10 s in Status 3, ist der Abbruch vertretbar, dann aber mit dem try/finally aus
      CP nach Fehler/Abbruch bei Phasenumschaltung und CP-Unterbrechung immer wieder verbinden #4037, damit CP in jedem Fall wieder verbunden wird. In allen anderen Fällen die GPIO-Sequenz ausführen und
      old_phases_in_use setzen.

    Bei mir läuft seit 06.11. eine Variante mit fester 5-s-Pause als Rückfallebene und Best-Effort-Rücklesen
    (Code im Issue oben), seither keine Auffälligkeit bei den Umschaltungen. Wenn Statuskriterium gewünscht ist,
    teste ich eine entsprechende Fassung gern an meiner series2 mit dem Audi.

  3. kaindl commented on Nov 11, 2026

    @kaindl
    Contributor

    Ah, macht auch irgendwie Sinn, dass man aus der evse nur die Vorgaben herauslesen kann, nicht die Ist-Werte ;)

    Das Problem, das ich bei deinem Ansatz sehe: Der Fehler, der diese Anpassung überhaupt erst ausgelöst hat, ist wieder drin und wird laut Kommentar auch akzeptiert ("Umschaltung wird trotzdem durchgeführt.").

    Eigentlich sollte die Logik aber ja genau andersherum ein Schalten der Schütze unter Last auf jeden Fall verhindern.
    Die aktuelle Logik prüft und wiederholt ja nur das Setzen in der evse

    with SingleComponentUpdateContext(self.fault_state, update_always=False, reraise=True):
        evse.set_current(0)
        for _ in range(20):  # poll up to 10s (20 × 0.5s) for EVSE to confirm 0 A
            _, _, evse_current = evse.get_plug_charge_state()
            if evse_current == 0:
                break
            time.sleep(0.5)
            evse.set_current(0)
        else:
            raise Exception("Ladung konnte nicht gestoppt werden - Phasenumschaltung abgebrochen.")
    GPIO.output(gpio_cp, GPIO.HIGH)  # CP off
    

    Demnach fehlt dann IMHO vor dem CP off noch eine tatsächliche Überprüfung, ob auch wirklich kein Strom mehr fließt und nur dann darf der CP unterbrochen werden:

    with SingleComponentUpdateContext(self.fault_state, update_always=False, reraise=True):
        evse.set_current(0)
        for _ in range(20):  # poll up to 10s (20 × 0.5s) for EVSE to confirm 0 A
            _, _, evse_current = evse.get_plug_charge_state()
            if evse_current == 0:
                break
            time.sleep(0.5)
            evse.set_current(0)
        else:
            raise Exception("Fehler in der Kommunikation mit der EVSE, Ladung konnte nicht gestoppt werden"
                            " - Phasenumschaltung abgebrochen.")
        for _ in range(10):  # poll up to 5s (10 × 0.5s) for Meter to confirm 0 A
            counter_state = self._client.meter_client.get_counter_state()
            if all(current == 0 for current in counter_state.currents):
                break
            time.sleep(0.5)
        else:
            raise Exception("Ladung konnte nicht gestoppt werden - Phasenumschaltung abgebrochen.")
    GPIO.output(gpio_cp, GPIO.HIGH)  # CP off
    

    Hast du Lust, das mal zu testen, wenn das für dich sinnvoll erscheint? Bei mir wird planmäßig erst am Wochenende die aktuelle openWB ohne Phasenumschaltung gegen eine mit Phasenumschaltung getauscht, dann kann ich auch gegen einen A6 e-tron testen.

  4. Stefan3934 commented on Nov 12, 2026

    @Stefan3934
    Author

    Einverstanden, der Zählerstrom ist das einzige Kriterium, das den Iststrom abbildet – die feste Pause war
    bei mir nur die Rückfallebene, weil der Sollwert nichts aussagt. Ich teste die Variante mit Zählerprüfung
    gern an meiner series2 (MPM3PM, Audi A6 e-tron). Geht bei mir auch erst am Samstag.

    Drei Dinge würde ich dabei anders machen:

    1. Lesefehler in der Schleife nicht zum Abbruch machen. Der Zähler hängt am selben RS485-Bus wie die
      EVSE; der Aussetzer vom 06.11. 13:15 (No response received) trifft get_counter_state() genauso. Mit
      reraise=True um die ganze Schleife bricht ein einzelner fehlgeschlagener Lesezugriff die Umschaltung ab,
      obwohl die nächste Abfrage 0,5 s später 0 A gezeigt hätte. Besser: Exception in der Schleife abfangen,
      weiter pollen, und erst abbrechen, wenn innerhalb des Zeitfensters keine Bestätigung 0 A kam. Damit
      bleibt deine Eigenschaft "ohne bestätigte 0 A wird nicht geschaltet" erhalten, ohne dass ein Busfehler
      allein reicht.

    2. Toleranzschwelle statt == 0. Der MPM3PM liefert zwar bei offenem Fahrzeugschütz exakt 0,0 A (siehe
      Logs oben), aber andere Zähler (SDM, B23) rauschen im Hundertstel-Bereich, und 0.0 == 0 auf Floats
      ist unnötig scharf. max(currents) < 0.5 o.ä. reicht; get_values() nutzt für phases_in_use ohnehin
      > 3 A.

    3. Zeitfenster 10 s statt 5 s. Nach IEC 61851-1 darf das EV bis 3 s brauchen, um auf PWM-Abschaltung zu
      reagieren; dazu kommen die Zählerabfragen selbst (B23 mit sleep(0.1) je Register, SDM ähnlich), so dass
      5 s nur etwa 5–6 Abfragen sind. Die EVSE-Schleife davor endet ja ohnehin beim ersten Durchlauf.

    Konkret (deine Fassung, nur die drei Punkte ergänzt plus das finally aus #4037):

    PHASE_SWITCH_CURRENT_THRESHOLD_A = 0.5
    PHASE_SWITCH_METER_POLLS = 20  # 20 × 0,5 s = 10 s
    
    def perform_phase_switch(self, phases_to_use: int) -> None:
        gpio_cp, gpio_relay = self._client.get_pins_phase_switch(phases_to_use)
        evse = self._client.evse_client
        with SingleComponentUpdateContext(self.fault_state, update_always=False, reraise=True):
            evse.set_current(0)
            for _ in range(20):  # poll up to 10s (20 × 0.5s) for EVSE to confirm 0 A
                _, _, evse_current = evse.get_plug_charge_state()
                if evse_current == 0:
                    break
                time.sleep(0.5)
                evse.set_current(0)
            else:
                raise Exception("Fehler in der Kommunikation mit der EVSE, Ladung konnte nicht gestoppt werden"
                                " - Phasenumschaltung abgebrochen.")
            for _ in range(self.PHASE_SWITCH_METER_POLLS):  # Zähler muss 0 A bestätigen
                try:
                    counter_state = self._client.meter_client.get_counter_state()
                    if max(counter_state.currents) < self.PHASE_SWITCH_CURRENT_THRESHOLD_A:
                        break
                except Exception as e:
                    # einzelner Busfehler: weiter pollen, Abbruch nur ohne Bestätigung im Zeitfenster
                    log.warning(f"Phasenumschaltung LP{self.local_charge_point_num}: Zähler nicht lesbar ({e})")
                time.sleep(0.5)
            else:
                raise Exception("Fahrzeug hat die Ladung nicht beendet (Zählerstrom > 0)"
                                " - Phasenumschaltung abgebrochen.")
        try:
            GPIO.output(gpio_cp, GPIO.HIGH)  # CP off
            GPIO.output(gpio_relay, GPIO.HIGH)  # 3 on/off
            time.sleep(5)
            GPIO.output(gpio_relay, GPIO.LOW)  # 3 on/off
            time.sleep(5)
        finally:
            GPIO.output(gpio_cp, GPIO.LOW)  # CP on
        time.sleep(1)
        self.old_phases_in_use = phases_to_use

    Zum Abbruchfall noch eine Beobachtung: perform_phase_switch läuft im Handler direkt als Thread-Target
    (__thread_phase_switch), die Exception landet im Log und im FaultState von LP0, aber nicht bei der
    Regelung. check_phase_switch_completed() gibt nach Thread-Ende den Strom frei und merkt den Fehlschlag
    erst nach keep_charge_active_duration über failed_phase_switches (Zähler-Phasenzahl ≠ phases_to_use).
    Bis dahin rechnet die Regelung mit der neuen Phasenzahl, die Hardware steht auf der alten. Mit
    Zählerprüfung wird der Abbruch seltener, verschwindet aber nicht – vielleicht ein Thema für einen
    separaten Fix.

    Testen kann ich frühestens am Samstag. Bis dahin gern Rückmeldung zu den drei Punkten, dann spiele ich die
    abgestimmte Fassung ein und melde mich mit Logs der ersten PV-Umschaltungen (1→3 unter Last und 3→1).

  5. kaindl commented on Nov 12, 2026

    @kaindl
    Contributor

    @Stefan3934 sieht gut aus, ich werds bei mir auch gleich drauf packen und voraussichtlich am Wochenende gegen A6 e-tron und einen 2025er ID.3 testen 👍

  6. seaspotter commented on Nov 12, 2026

    @seaspotter
    Collaborator

    Zwei Dinge fallen mir an der aktuellen Fassung noch auf:

    1. Die EVSE-Schleife (get_plug_charge_state()) liest weiterhin nur das Register zurück, das set_current() selbst gerade geschrieben hat - sie bestätigt also nichts und bricht bei einem einzelnen Modbus-Fehler dort weiterhin die ganze Umschaltung ab (reraise=True), nur eben über den EVSE-Bus statt den Zähler-Bus.
    2. max(counter_state.currents) kann crashen, nicht nur falsch liegen: B23 liefert bei nicht angeschlossener Phase None (check_nan()), max() auf einer Liste mit None wirft TypeError. Zusätzlich: SDM implementiert get_currents() gar nicht separat, nur get_counter_state() - falls das noch irgendwo direkt genutzt wird, kommt nur [0]*3 vom Abstract-Default zurück.

    Vorschlag, der beides behebt (EVSE-Schleife komplett raus, nur noch der Zähler-Poll als echtes Kriterium, None-sicher):

    PHASE_SWITCH_CURRENT_THRESHOLD_A = 0.5
    PHASE_SWITCH_METER_POLLS = 20  # 20 x 0.5s = 10s
    
    def perform_phase_switch(self, phases_to_use: int) -> None:
        gpio_cp, gpio_relay = self._client.get_pins_phase_switch(phases_to_use)
        evse = self._client.evse_client
        meter = self._client.meter_client
        with SingleComponentUpdateContext(self.fault_state, update_always=False, reraise=True):
            evse.set_current(0)
            for _ in range(self.PHASE_SWITCH_METER_POLLS):
                try:
                    currents = meter.get_counter_state().currents
                    if max((c for c in currents if c is not None), default=0) < self.PHASE_SWITCH_CURRENT_THRESHOLD_A:
                        break
                except Exception as e:
                    log.warning(f"Phasenumschaltung LP{self.local_charge_point_num}: Zähler nicht lesbar ({e})")
                time.sleep(0.5)
                try:
                    evse.set_current(0)
                except Exception as e:
                    log.warning(f"Phasenumschaltung LP{self.local_charge_point_num}: 0A setzen fehlgeschlagen ({e})")
            else:
                raise Exception("Fahrzeug hat die Ladung nicht beendet (Zählerstrom > 0)"
                                " - Phasenumschaltung abgebrochen.")
        try:
            GPIO.output(gpio_cp, GPIO.HIGH)  # CP off
            GPIO.output(gpio_relay, GPIO.HIGH)  # 3 on/off
            time.sleep(5)
            GPIO.output(gpio_relay, GPIO.LOW)  # 3 on/off
            time.sleep(5)
        finally:
            GPIO.output(gpio_cp, GPIO.LOW)  # CP on
        time.sleep(1)
        self.old_phases_in_use = phases_to_use
    

    Das try/finally ist aus #4037 vorweggenommen (dort noch offen) - sonst würden sich beide PRs an derselben Stelle überschneiden.

  7. Stefan3934 commented on Nov 13, 2026

    @Stefan3934
    Author

    @seaspotter beide Punkte stimmen, danke. Die EVSE-Schleife hatte ich drin gelassen, um nah an
    kaindls Fassung zu bleiben; als Kriterium taugt sie nicht, und als zweite Abbruchquelle ist sie sogar
    schädlich. Das None vom B23 (check_nan()) hatte ich übersehen. Deine Fassung ist aus meiner Sicht
    die richtige Basis, ich spiele sie am Samstag so ein.

    Zwei Ergänzungen würde ich noch vorschlagen:

    1. Zusätzlich auf den EVSE-Status warten (Register 1002 nicht 3/4). Der Zähler beweist 0 A, aber
      nicht, dass das Fahrzeug sein Schütz geöffnet hat und in State B ist. Ein EV, das bei 0 A in State C
      verharrt (Ladepause, Balancing), sieht beim CP-Trennen denselben Abriss wie unter Last. Das Statusregister
      ist genau die Information, die der EVSE-Schleife gefehlt hat. get_plug_charge_state() liest es bereits,
      gibt es aber nicht zurück; am wenigsten invasiv wäre, den Rohstatus wie evse_current als Attribut zu
      merken:

      # modules/common/evse.py, get_plug_charge_state()
      self.evse_current = int(raw_set_current)
      self.state_number = int(state_number)

      und in der Schleife beide Kriterien zu verlangen (Lesefehler wie beim Zähler nur loggen):

      for _ in range(self.PHASE_SWITCH_METER_POLLS):
          try:
              currents = meter.get_counter_state().currents
              werte = [c for c in currents if c is not None]
              evse.get_plug_charge_state()          # aktualisiert evse.state_number (wirft bei Status 5)
              if (werte and max(werte) < self.PHASE_SWITCH_CURRENT_THRESHOLD_A
                      and evse.state_number not in (3, 4)):
                  break
          except Exception as e:
              log.warning(f"Phasenumschaltung LP{self.local_charge_point_num}: Rücklesen fehlgeschlagen ({e})")
          time.sleep(0.5)
          try:
              evse.set_current(0)
          except Exception as e:
              log.warning(f"Phasenumschaltung LP{self.local_charge_point_num}: 0A setzen fehlgeschlagen ({e})")
      else:
          raise Exception("Fahrzeug hat die Ladung nicht beendet (Zählerstrom > 0 oder EVSE-Status C)"
                          " - Phasenumschaltung abgebrochen.")

      Falls ihr den EVSE-Zugriff in der Schleife lieber ganz vermeiden wollt, ist der Zähler allein immer
      noch deutlich besser als der Stand in 2.3.0; dann nur Punkt 2.

    2. default=0 nicht als Bestätigung werten. Liefert der Zähler für alle drei Phasen None
      (Lesefehler, der nicht als Exception ankommt), ergibt max(..., default=0) 0 A und die Umschaltung
      läuft ohne echte Messung. Mit werte and max(werte) < … wie oben gilt "keine Messung = keine
      Bestätigung", und nach dem Zeitfenster greift der Abbruch.

    Zum Zeitfenster: mit den sleep(0.1) je Register im B23-/SDM-Treiber dauert ein Schleifendurchlauf eher
    1 s als 0,5 s, 20 Durchläufe sind dann bis zu 20 s. Für den Abbruchfall ist das in Ordnung, nur damit es
    beim Lesen der Logs nicht überrascht.

    Ich melde mich nach dem Wochenende mit Logs beider Richtungen (1→3 unter Last, 3→1) von series2 + A6 e-tron.

  8. Stefan3934 commented on Nov 14, 2026

    @Stefan3934
    Author

    Testergebnis vom Samstagvormittag (14.11.2026), openWB series2, EVSE-Firmware 18, MPM3PM-Zähler, Audi A6 Avant e-tron, 2.3.0 mit der Fassung von @seaspotter plus den beiden Ergänzungen aus meinem letzten Kommentar (EVSE-Statusregister 1002 nicht 3/4, "alle None" zählt nicht als 0 A). Beide Richtungen unter voller Last, Umschaltung per "Anzahl Phasen" im Sofortladen.

    3 → 1 Phasen, Auto zieht 15,6 / 9,7 / 15,9 A:

    08:55:10,873 Phasenumschaltung LP0: Durchlauf 1, Ströme [15.58, 9.74, 15.87] A, EVSE-Status 3
    08:55:12,188 Phasenumschaltung LP0: Durchlauf 2, Ströme [0.0, 0.857, 0.013] A, EVSE-Status 2
    08:55:13,463 Phasenumschaltung LP0: Durchlauf 3, Ströme [0.0, 0.0, 0.0] A, EVSE-Status 2
    08:55:13,468 Phasenumschaltung LP0: 0 A und EVSE-Status 2 nach 3 Durchläufen / 3.5 s bestätigt
    08:55:24,484 Phasenumschaltung LP0: auf 1 Phasen umgeschaltet (14.5 s gesamt)
    08:55:42     Auto lädt wieder, 15,3 A auf L1
    

    1 → 3 Phasen, Auto zieht 14,8 A auf L1:

    08:57:18,520 Phasenumschaltung LP0: Durchlauf 1, Ströme [14.84, 0.0, 0.0] A, EVSE-Status 3
    08:57:19,783 Phasenumschaltung LP0: Durchlauf 2, Ströme [0.013, 0.0, 0.0] A, EVSE-Status 2
    08:57:19,788 Phasenumschaltung LP0: 0 A und EVSE-Status 2 nach 2 Durchläufen / 2.1 s bestätigt
    08:57:30,808 Phasenumschaltung LP0: auf 3 Phasen umgeschaltet (13.1 s gesamt)
    08:57:52     Auto lädt wieder, 15,7 / 13,3 / 16,0 A
    

    Kein Busfehler, kein Abbruch, kein "Ladenetzfehler" am Audi, beide Male lädt das Auto nach der Umschaltung ohne Zutun weiter. Ein Durchlauf dauert hier mit MPM3PM rund 1,3 s, der Audi hat die Ladung in beiden Fällen innerhalb von 2 s beendet.

    Durchlauf 2 der ersten Umschaltung ist aus meiner Sicht das interessanteste Detail: Die EVSE meldet schon Status 2 (State B), auf L2 fließen aber noch 0,86 A. Erst einen Durchlauf später ist der Zähler wirklich bei 0. Das spricht dafür, beide Kriterien zu verlangen (Zähler unter 0,5 A und Status nicht 3/4) und bei "fast 0" lieber einen Durchlauf länger zu warten; der Zähler allein mit == 0 hätte hier ebenfalls erst in Durchlauf 3 ausgelöst, der EVSE-Status allein schon in Durchlauf 2 unter Restlast.

    Der Vollständigkeit halber: In der Nacht davor (Zeitladen, Umschaltung ohne Last beim Planstart) lief dieselbe Fassung ebenfalls unauffällig.

    Unabhängig davon bin ich beim Testen über ein anderes Problem gestolpert, das nichts mit der Umschaltung zu tun hat: Die Wiederholung der Control-Pilot-Unterbrechung ("Auto lädt trotz Freigabe nicht") feuert in 2.3.0 in jedem Regelzyklus statt nach dem Wiederholintervall, weil control_parameter/timestamp_last_cp_retry in setdata.py nicht validiert und damit nie übernommen wird, und timestamp_charge_start beim Ladestopp nicht zurückgesetzt wird. Das hat den Audi heute zweimal in den Ladenetzfehler getrieben, bevor die Umschaltung überhaupt dran war. Dafür mache ich ein eigenes Issue auf, damit es hier nicht vermischt wird.

    Falls hilfreich, stelle ich das vollständige Log (main.log + internal_chargepoint.log mit DEBUG, 08:33 bis 08:59) gern zur Verfügung.

  9. seaspotter commented on Nov 14, 2026

    @seaspotter
    Collaborator

    Klingt doch ganz vielversprechend, Danke für den Test und die Zusammenfassung.

    Da der cp_retry eh von mir ist, hab ich das gleich als PR eingereicht um das zu fixen, danke fürs Melden. Brauchst also kein Issue aufmachen. #4329

    Für die Phasenumschaltung machst du n PR oder bei Bedarf kann ich auch unterstützen oder es machen, will euch die Arbeit aber nicht wegnehmen, ist nur n Angebot :)

  10. kaindl commented on Nov 14, 2026

    @kaindl
    Contributor

    Danke für die Tests und die Umsetzung 😊

    Habs vorher mit der Version aus dem PR mit A6 e-tron und ID.3 erfolgreich getestet 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions