Skip to content

Speicher-Puffer: laufende Ladung halten statt zurückschalten oder abschalten - #3997

Draft
s0170071 wants to merge 2 commits into
openWB:masterfrom
s0170071:feat/bat-buffer-hold-charge
Draft

s0170071 wants to merge 2 commits into
openWB:masterfrom
s0170071:feat/bat-buffer-hold-charge

Conversation

@s0170071

@s0170071 s0170071 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Im Modus „Mindest-SoC des Speichers" gibt get_charging_power_left_diff() oberhalb des Maximal-SoC die konfigurierte Entladeleistung als Überschuss frei:

# packages/control/bat_all.py:403
charging_power_left = min(required_absolut_discharge_power + base_power,
                          absolute_bat_discharge_power)

Der Zweck ist, eine laufende Überschuss-Ladung über Wolkenlücken hinweg zu stützen, bis der Mindest-SoC wieder erreicht ist. Die Regelung arbeitet in genau diesem Zustand aber gegen den Puffer:

Mehrphasige LP mit automatischer Umschaltung. Die freigegebene Entladeleistung ist typischerweise kleiner als der Bedarf eines dreiphasig ladenden Fahrzeugs. Der Überschuss ist damit ≤ 0 — und das ist die Rückschaltbedingung:

# packages/control/ev/ev.py:281
condition_3_to_1 = get_medium_charging_current(
    get_currents) < min_current_range and surplus <= 0 and phases_in_use > 1

Nach der Rückschaltung lädt das Fahrzeug mit einem Drittel der Leistung. Der Puffer hat dann zwar leichtes Spiel, aber die Ladung, die er halten sollte, ist bereits eingebrochen. Kommt die Sonne zurück, kostet die Hochschaltung erneut switch_on_delay plus phase_switch_delay.

Ein- und festphasige LP. Hier gibt es keine Umschaltung, die greifen könnte. Stattdessen läuft direkt die Abschaltverzögerung an, weil switch_off_check_threshold() die Schwelle überschritten sieht. Bei mehrphasigen LP verdeckt phase_switch_necessary() das bisher, bei diesen nicht.

In beiden Fällen ist das Ergebnis dasselbe: Der Speicher gibt Leistung frei, die die Regelung nicht in die Ladung umsetzt.

Und wenn der Puffer leer läuft, stapeln sich die Verzögerungen. Die Rückschaltung hat Vorrang vor der Abschaltung:

# packages/control/algorithm/surplus_controlled.py:152
if ((control_parameter.state == ChargepointState.CHARGING_ALLOWED or
        control_parameter.state == ChargepointState.SWITCH_OFF_DELAY) and
        phase_switch_necessary() is False):
    evu_counter.switch_off_check_threshold(load)

phase_switch_necessary() ist rein statisch — Hardware kann umschalten, aktuell nicht einphasig, Automatikmodus. Ein dreiphasiger Automatik-LP erreicht die Abschaltschwelle also überhaupt nie, solange er dreiphasig lädt. Unterschreitet der Speicher den Mindest-SoC, läuft deshalb ab:

  1. switch_off_delay für die Rückschaltung 3 → 1 (ev.py:334), dazu der Puffer phase_switch_delay seit der letzten Umschaltung
  2. danach einphasig, und jetzt erst greift die Abschaltschwelle — mit noch einmal switch_off_delay

Die ganze Zeit lädt das Fahrzeug aus dem Netz, während der Speicher sich eigentlich wieder aufladen soll.

Änderung

BatAll.is_buffering() und BatAll.is_buffer_depleted()

_buffer_configured() prüft, ob der Speicher überhaupt als Puffer wirken kann — konfiguriert, kein Fehlerfall, Modus min_soc_bat_mode, power_discharge_active und die Freigabe der aktiven Speichersteuerung. Dieselbe Bedingung, unter der get_charging_power_left_diff() die Entladeleistung zuschlägt. Darauf setzen zwei Zustände auf:

  • is_buffering() — zusätzlich hysteresis_discharge (Maximal-SoC erreicht, Mindest-SoC noch nicht wieder unterschritten) und keine vorgegebene Entladeleistung. Der Puffer stützt die Ladung.
  • is_buffer_depleted() — zusätzlich soc < min_soc. Der Puffer ist aufgebraucht, der Speicher hat Vorrang und lädt sich wieder auf.

Die Modus-Prüfung ist wichtig: hysteresis_discharge wird nur im Mindest-SoC-Modus gepflegt und ist in bat_mode/ev_mode ein Altwert.

Rein additiv, get_charging_power_left_diff() bleibt unangetastet. Die Bedingung ist dort zweimal ausgeschrieben (Band und oberhalb Maximal-SoC); sie gegen den neuen Helper zu deduplizieren wäre naheliegend, kollidiert aber mit #3985 und #3986, die genau diese Zweige anfassen. Das gehört in einen Folge-PR, wenn die beiden durch sind.

Sperre der Rückschaltung 3 → 1

Früher Ausstieg in _check_phase_switch_conditions(), bevor die Bedingungen gerechnet werden:

if phases_in_use > 1 and control_parameter.submode == Chargemode.PV_CHARGING:
    if data.data.bat_all_data.is_buffering():
        return False, self.BAT_BUFFERING
    if data.data.bat_all_data.is_buffer_depleted():
        return False, self.BAT_BUFFER_DEPLETED

Auf phases_in_use > 1 begrenzt, die Hochschaltung 1 → 3 bleibt also unberührt: wenn genug Überschuss für drei Phasen da ist, soll auch hochgeschaltet werden, unabhängig vom Speicher. Auf den PV-Submodus begrenzt, damit Sofort- und Zielladen nicht betroffen sind.

Über den bestehenden Rückgabepfad bricht das auch eine bereits laufende PHASE_SWITCH_DELAY ab und gibt reserved_surplus zurück — das ist der gewünschte Effekt, wenn der Puffer mitten in der Verzögerung einsetzt oder leer läuft.

Leerer Puffer: abschalten statt zurückschalten

phase_switch_necessary() gibt bei leerem Puffer False zurück, die Abschaltschwelle greift also wieder. Zusammen mit der obigen Unterdrückung von condition_3_to_1 bleibt genau eine switch_off_delay stehen statt der Kaskade aus Rückschaltverzögerung, phase_switch_delay und Abschaltverzögerung.

Das ist auch inhaltlich richtig: unterhalb des Mindest-SoC hat der Speicher Vorrang („Speicher-Vorrang bis zum Min-Soc", bat_all.py:359), und regulate_up setzt dort ohnehin schon die Abschaltbedingung in counter.py. Einphasig mit 6 A weiterzuladen sind 1,4 kW, die dem Speicher beim Wiederaufladen fehlen — und die Rückschaltung kostet den Weg dorthin zusätzlich Zeit. Erholt sich der Puffer, entscheidet die normale Einschaltlogik neu, ob ein- oder dreiphasig gestartet wird.

Die verbleibende switch_off_delay ist bewusst nicht wegoptimiert: sie filtert kurze Einbrüche in der Überschussmessung, und dieser Filter soll erhalten bleiben.

Veto gegen die Abschaltschwelle

_bat_buffer_holds_charge() in counter.py prüft zusätzlich zu is_buffering():

  • nur Ladepunkte — für Verbraucher gilt weiterhin die Verbraucher-Abschaltschwelle, sie sind nicht gemeint
  • nur bei tatsächlich fließender Ladung (current_prev or charge_state) — der Puffer soll eine laufende Ladung halten, aber keine neue starten

In switch_off_check_threshold() bricht das eine laufende Abschaltverzögerung ab (über die vorhandene Buchhaltung released_surplus -= required_power, Zustand CHARGING_ALLOWED) und verhindert, dass eine neue gestartet wird.

Das ist vor allem für ein- und festphasige LP nötig. Bei mehrphasigen Automatik-LP hat die Phasensperre ohnehin Vorrang: solange der Puffer stützt, ist is_buffer_depleted() falsch, und phase_switch_necessary() lässt die Abschaltschwelle dort gar nicht erst erreichen.

Was nicht nötig war

Ein expliziter 6-A-Boden. limit_adjust_current() liefert bereits strukturell nie weniger als min_current:

# packages/control/algorithm/surplus_controlled.py:221
return max(current,
           load.data.control_parameter.min_current,
           load.data.set.target_current)

Solange der LP lädt und nicht abgeschaltet wird, liegt der Strom also nicht unter dem Mindeststrom. Genau das leisten die beiden Vetos: sie verhindern das Abschalten und das Zurückschalten, der Boden ergibt sich.

Ablauf über einen Puffer-Zyklus

SoC is_buffering() is_buffer_depleted() Verhalten bei Überschuss ≤ 0
≥ Maximal-SoC, dann fallend wahr falsch dreiphasig weiterladen, Speicher stützt
< Mindest-SoC falsch wahr eine switch_off_delay, dann aus
wieder aufladend, < Maximal-SoC falsch falsch bei ≥ Mindest-SoC Einschaltschwelle entscheidet neu, Phasenzahl inklusive

Der Umschlagpunkt selbst bleibt, wo er ist — dieser PR ändert nur, was dort passiert.

Test

Sechsundzwanzig neue Fälle:

  • bat_all_test.py::test_is_buffering — alle Bedingungen einzeln, inklusive der Altwert-Fälle in bat_mode/ev_mode
  • bat_all_test.py::test_is_buffer_depleted — Grenze am Mindest-SoC, kein Puffer ohne freigegebene Entladeleistung
  • ev_test.py::test_check_phase_switch_conditions_bat_buffering — Sperre greift, greift ohne Puffer nicht, lässt 1 → 3 durch, greift außerhalb des PV-Submodus nicht
  • ev_test.py::test_check_phase_switch_conditions_bat_buffer_depleted — keine Rückschaltung bei leerem Puffer, unverändert ohne
  • counter_test.py::test_switch_off_check_threshold_bat_buffering — Abbruch einer laufenden Verzögerung, kein Start einer neuen, unveränderte Wege ohne Puffer, keine Wirkung ohne fließende Ladung
  • surplus_controlled_test.py::test_check_submode_pv_charging_buffer_depleted — die Abschaltschwelle wird bei leerem Puffer auch für einen dreiphasigen Automatik-LP geprüft, sonst nicht

Gegenprobe mit neutralisierten Vetos: genau die sieben positiven Fälle schlagen fehl, die Negativkontrollen bleiben grün.

python -m pytest packages/control -q
  master:      17 failed, 467 passed
  dieser PR:   17 failed, 493 passed

Dieselben 17 Fehlschläge (ocpp_test.py, charge_template_test.py), sie bestehen auf master unverändert. flake8 sauber.

Bezug: #3980

🤖 Generated with Claude Code

s0170071 and others added 2 commits September 23, 2026 09:21
…schalten

Wenn der Speicher im Modus "Mindest-SoC des Speichers" oberhalb des
Maximal-SoC seine freigegebene Entladeleistung als Ueberschuss anbietet,
soll er eine laufende Ueberschuss-Ladung ueber Wolken hinweg stuetzen.
Die Regelung arbeitet aber dagegen:

- Die Entladeleistung deckt bei mehrphasigem Laden nur einen Teil des
  Bedarfs, der Ueberschuss ist also <= 0 und damit die Bedingung fuer die
  Rueckschaltung 3 -> 1 erfuellt. Einphasig ist die Ladeleistung ein
  Drittel und der Puffer damit wirkungslos.
- Ein- und festphasige Ladepunkte haben keine Umschaltung, die greifen
  koennte; bei ihnen laeuft stattdessen die Abschaltverzoegerung an.

BatAll.is_buffering() macht diesen Zustand abfragbar - exakt die
Bedingung, unter der get_charging_power_left_diff() die Entladeleistung
freigibt. Solange sie wahr ist:

- sperrt Ev._check_phase_switch_conditions() die Rueckschaltung 3 -> 1
  (die Hochschaltung 1 -> 3 bleibt unberuehrt),
- haelt Counter.switch_off_check_threshold() eine tatsaechlich fliessende
  Ladung gegen die Abschaltschwelle und bricht eine bereits laufende
  Abschaltverzoegerung ab.

Der Mindeststrom selbst ist bereits strukturell gesichert:
limit_adjust_current() gibt nie weniger als min_current zurueck.

Verbraucher sind nicht betroffen, fuer sie gilt weiterhin die
Verbraucher-Abschaltschwelle. Eine Ladung, die noch gar nicht laeuft,
startet der Puffer nicht.

Co-Authored-By: Claude Opus 5 <[email protected]>
Die Rueckschaltung hat Vorrang vor der Abschaltung: surplus_controlled.py
ueberspringt switch_off_check_threshold(), solange phase_switch_necessary()
gilt. Diese Bedingung ist rein statisch (HW kann umschalten, aktuell nicht
einphasig, Automatikmodus), ein dreiphasiger Automatik-LP erreicht die
Abschaltschwelle also nie.

Wenn der Puffer leer laeuft, stapeln sich dadurch die Verzoegerungen: fuer
3 -> 1 gilt switch_off_delay, dazu der Puffer phase_switch_delay seit der
letzten Umschaltung, und danach wartet die Abschaltschwelle noch einmal
switch_off_delay ab. Die Ladung laeuft die ganze Zeit aus dem Netz, waehrend
der Speicher sich eigentlich wieder aufladen soll.

Unterhalb des Mindest-SoC ist die Rueckschaltung auch nicht die richtige
Reaktion: einphasig weiterzuladen konkurriert weiter mit dem Speicher, der
dort Vorrang hat. BatAll.is_buffer_depleted() macht diesen Zustand
abfragbar; solange er gilt,

- unterdrueckt Ev._check_phase_switch_conditions() die Rueckschaltung 3 -> 1,
- laesst phase_switch_necessary() die Abschaltschwelle durch.

Damit bleibt genau eine switch_off_delay stehen. Erholt sich der Puffer,
entscheidet die Einschaltlogik neu, ob ein- oder dreiphasig gestartet wird.

Co-Authored-By: Claude Opus 5 <[email protected]>
@seaspotter

Copy link
Copy Markdown
Collaborator

Ich häng mal eine Frage dran, bevor ich in den Code im Detail einsteige:

Mit diesem PR haben wir jetzt drei noch offene, sich gegenseitig beeinflussende Änderungen an min_soc_bat_mode gleichzeitig in der Pipeline (#3985, #3986, dieser hier). #3985/#3986 beheben einen klaren Bug (Hysterese erholt sich nie bis Maximal-SoC) - das ist eine Korrektur an bereits dokumentiertem Verhalten. Dieser PR hier ist dagegen etwas anderes: eine neue, eigenständige Pufferlogik oberhalb davon (Phasenrückschaltung unterdrücken, Abschaltschwelle veto'en, solange der Speicher stützt). Das geht über das hinaus, was die Wiki-Doku für den Modus aktuell beschreibt ("Wird der Mindest-SoC überschritten, wird der Überschuss ins Fahrzeug geladen.") - für mich eher ein neues Feature als ein Bugfix am bestehenden Verhalten.

Das hattest du im Issue ja selbst schon offen gelassen ("Was ich so aus dem FF nicht beurteilen kann ist ob die Änderungen #3986 + #3985 das Auto bei höchstens 6A halten und die 3p->1p Umschaltung unterdrücken solange die Hausbatterie puffert. Das wäre noch zu prüfen.") - und genau da hänge ich mich ein: Ich würde vorschlagen, das erst zu prüfen (#3985/#3986 landen lassen, dann schauen, ob eine sinnvoll abgestimmte switch_off_delay allein für die meisten Fälle schon reicht), statt direkt einen fest programmierten Halte-/Unterdrückungsmechanismus obendrauf zu setzen.

Unabhängig vom Timing steht für mich noch eine grundsätzlichere Frage im Raum: Soll dieses Pufferverhalten Teil von min_soc_bat_mode sein (und damit automatisch für alle gelten, die den Modus schon heute für die einfache, dokumentierte Schwellenwert-Logik nutzen), oder wäre das eher ein eigener, optionaler Modus/Schalter? Wer aktuell min_soc_bat_mode einsetzt, erwartet die dokumentierte Schwelle - nicht, dass Phasenumschaltung und Abschaltverzögerung des Fahrzeugs plötzlich vom Speicherzustand mitgesteuert werden. Das würde ich gerne von @ndrsnhs eingeordnet haben, bevor wir hier tiefer in Details/Review gehen - lieber einmal die Richtung klären, als drei PRs parallel gegeneinander zu iterieren.

Nichts davon ist eine inhaltliche Kritik am Ansatz - dein 4-Schritte-Modell aus dem Issue hat sich bei dir im Betrieb ja klar bewährt. Mir geht's nur um Reihenfolge und darum, dass wir uns nicht in drei überlappenden PRs zum selben Modus verheddern, bevor überhaupt geklärt ist, was der Modus eigentlich leisten soll.

@s0170071

Copy link
Copy Markdown
Contributor Author

Huch, das ging ja schnell.

Kurzum, der Modus soll den Hausakku als Kurzzeitpuffer verwenden. Anstatt der Wartezeiten.

Bisher:

  • Wolke kommt. Akku irgendwo knapp >minSoC. Fahrzeig lädt weiter, Akku fällt unter minSoC.
  • Fahrzeug lädt weiter wie bisher. Zumindest kann ich das im code nicht nachvollziehen mit welchem Strom geladen wird.
  • Wolke weg: Akku bleibt < minSoc. (Das machen die zwei anderen PRs weg )
  • generell machend die Wartezeiten bei lockerer Bewölkung Schwierigkeiten. Da kommt es auch schon mal vor dass das Fahrzeug gerade abgeschaltet oder gephasenwechselt hat, dann brezelt die Sonne plötzlich auf die Module und weder Hausakku noch Fahrzeug können ausreichend aufnehmen. Da ist es besser, das Fahrzeug ist schon mit 3p am Laden und kann dann schnell hochrampen.

Neu:

  • das Auto höchstens mit 6A lädt wenn Hausakku puffert
  • keine Phasenumschaltung 3-1 wenn gepuffert wird. Würde 3p->1p stattfinden, ist das ja nix anderes als abschalten, der Akku ist <min und man schaltet trotzdem schonmal zu. Zwar nur einphasig aber je nach Wolke kann das auch schon zu viel sein.
  • Verzögerungen (einschalt / abschalt) kann man auf 0 setzen. Das macht die Wallbox deutlich mehr "responsiv ?" . Anstecken-> lädt. Die Zeitkonstante, die dein Hausakku von min->max hat ist implizit die neue Ab- und Zuschaltdauer.

@seaspotter

seaspotter commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Nochmal, diese Funktion ist aber nirgens so dokumentiert/beschrieben das es das tut. Das ist ein Wunsch von dir und daher ist es hier doch erstmal wichtig zu klären ob das der bestehende Modus überhaupt tun soll oder dafür nicht lieber ein neuer Modus implementiert wird.
Bevor man das also implementiert und nen PR stellt, sollte das doch lieber erstmal diskutiert werden? Das war die Absicht meines Kommentars.

@s0170071

s0170071 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Gerne doch. Das betrifft jetzt aber nur diese PR , nicht #3985 und #3986.
Die Diskussion ist dann wohl #3980 oder zusätzlich irgendwo im Forum ?

@seaspotter

Copy link
Copy Markdown
Collaborator

#3980 ist imho der richtige Ort dafür

@LKuemmel LKuemmel linked an issue Sep 23, 2026 that may be closed by this pull request
@LKuemmel
LKuemmel requested a review from ndrsnhs September 23, 2026 11:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Haus Akku zum Puffern bei Bewölkung verwenden.

2 participants