You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
external_openwb: durchgehende MQTT-Verbindung statt Neuverbindung pro Zyklus - #3996
Auf einem Produktivsystem trat die ganze Nacht wiederholt folgender Fehler für einen externen Ladepunkt (external_openwb, Secondary per Netzwerk angebunden) auf:
set values cp5 konnte nicht innerhalb des Timeouts abgearbeitet werden.
Ursache: get_values() baut pro Regelzyklus 6 einzelne, kurzlebige MQTT-Verbindungen zur Gegenstelle auf (5x pub_single() über paho.mqtt.publish.single(), das bei jedem Aufruf neu verbindet/trennt, plus 1x BrokerClient(...).start_finite_loop() zum Abonnieren). All das musste bisher gemeinsam in das enge Timeout aus loadvars.py passen (control_interval/3, bei Standard 10s also ~3,3s) - schon normaler Netzwerk-Jitter zur Gegenstelle konnte das reißen.
Das wurde durch #3976 verschärft, nicht verursacht:#3976 hat das interne Timeout von start_finite_loop() bewusst von 1s auf 2s erhöht, um ein anderes, echtes Problem zu beheben (gelegentlich fehlende retained Topics unter Last). Das war für sich genommen richtig - aber niemand hat das äußere Timeout in loadvars.py entsprechend mit angepasst. Vor #3976 blieben von den ~3,3s noch ~2,3s Puffer für die 6 Verbindungsaufbauten übrig, nach #3976 nur noch ~1,3s. Damit wurde genau der Fehler, den dieser PR behebt, durch #3976 spürbar wahrscheinlicher.
Die Lösung
Statt pro Zyklus neu zu verbinden, hält PersistentBrokerClient (neu, helpermodules/broker.py) eine einzelne, dauerhafte, automatisch wiederverbindende MQTT-Verbindung pro Gegenstelle offen - wird von allen pub_single()-Aufrufen zu diesem Host genutzt und bleibt durchgehend abonniert, statt pro Zyklus neu zu verbinden/abzumelden. Das ist kein neues Muster: Der komplette Datenkern von openWB (SubData, command.py, setdata.py, der simpleAPI-MQTT-Daemon) funktioniert schon heute nach exakt diesem Prinzip - nur bisher ausschließlich für den lokalen Broker. Neu ist hier lediglich, dieses Prinzip auch für Verbindungen zu einer entfernten Gegenstelle zu nutzen (inkl. reconnect_delay_set, das die bestehenden lokalen Verbindungen nicht brauchen, weil der lokale Broker als praktisch immer verfügbar gilt).
publish() wirft jetzt explizit einen Fehler, wenn die Verbindung nicht steht (pahos Client.publish() schlägt bei fehlender Verbindung sonst nur mit einem Rückgabecode fehl, ohne Exception) - sonst hätte die bestehende client_error_context-Sicherheitsabschaltung (Soll-Strom nach 60s anhaltendem Fehler auf 0) nie mehr ausgelöst.
wait_for_fresh_data() liefert weiterhin ein leeres Dict (nicht die letzten bekannten Werte), wenn in diesem Zyklus nichts Neues angekommen ist - die bestehende "keine Daten empfangen"-Warnung funktioniert dadurch unverändert.
Die Fehlermeldung bei einer nicht erreichbaren Gegenstelle nutzt jetzt den vorhandenen, übersetzten OSError-Handler (Die Verbindung zum Host ist fehlgeschlagen. Überprüfe Adresse und Netzwerk.) statt einer nichtssagenden Fallback-Meldung.
Getestet
Unit-Tests für PersistentBrokerClient (broker_test.py, gemockter mqtt.Client, keine echten Sockets): Verbindungsprüfung/Exception, Abo-Deduplizierung, Resubscribe nach Reconnect, Timing-Logik von wait_for_fresh_data().
Live gegen ein echtes Testsystem mit echtem Secondary-openWB über Netzwerk getestet:
Ladepunkt frisch angelegt, IP noch nicht gespeichert → sauberer, sich selbst korrigierender Zustand, kein Absturz.
Ursprünglicher Fehler (Timeout) über mehrere Regelzyklen: nicht ein einziges Mal aufgetreten.
Neustart des Secondary-Dienstes: unterbrechungsfrei erkannt, selbstständig erholt.
Kurze (~1 min) und lange (~3 min) echte Netzwerkunterbrechung (Firewall-Block): korrekte Eskalationskette - Warnung → erkannter Verbindungsfehler → Sicherheitsabschaltung nach 60s (Soll-Strom auf 0) → FaultState eskaliert auf ERROR → vollständige, selbstständige Erholung nach Freigabe, ohne Neustart des Primary-Dienstes.
Was nicht getestet wurde
Zwei Ladepunkte an einem Secondary (duo_num 0/1, gemeinsame Verbindung) - die Testumgebung hatte nur einen konfiguriert. Sollte durch das Verbindungs-Caching pro Host funktionieren, war aber nicht live zu verifizieren.
Eine tatsächlich laufende Ladung, die durch einen Verbindungsabbruch unterbrochen wird - verifiziert wurde, dass der Soll-Strom korrekt auf 0 gesetzt/gesendet wird, nicht der tatsächliche Ladeabbruch an der Hardware.
…connection
cp threads were timing out (loadvars.py's control_interval/3 budget)
because get_values() opened 6 fresh TCP+MQTT connections per cycle to
the remote host - already tight before openWB#3976 raised the internal wait
from 1s to 2s to fix missed retained topics, which shrank the margin
further and made timeouts common on any network jitter.
Replaces this with one long-lived, auto-reconnecting connection per
remote host (PersistentBrokerClient), shared by all pub_single() calls
to that host and kept continuously subscribed instead of resubscribing
every cycle. Preserves existing safety semantics: publish() now raises
when the socket is down (paho's Client.publish() fails silently
otherwise, which would have broken the exception-driven
client_error_context shutoff), and wait_for_fresh_data() still returns
an empty dict - not stale cached data - when nothing new arrived this
cycle, so the "no data received" fault_state warning still fires
correctly.
…d of "Unbekannter Fehler"
publish() raised a plain ConnectionError(message), but the existing
OSError fault-state translator (helpermodules/exceptions/os.py) only
reads e.errno/e.strerror, so the message was silently discarded and
fell back to its generic "OSError None: Unbekannter Fehler None"
template - confirmed live during today's 3-minute network-outage test.
Construct it as ConnectionError(errno.EHOSTUNREACH, message) instead,
so it hits that translator's existing EHOSTUNREACH special case and
produces the already-established "Die Verbindung zum Host ist
fehlgeschlagen. Überprüfe Adresse und Netzwerk." message.
…adepunkten beheben (Review Copilot)
publish() wertete is_connected() sofort nach dem Konstruktor aus, obwohl
connect_async()/loop_start() asynchron verbinden - der allererste Aufruf an
einen neuen Host loeste dadurch nach jedem Neustart praktisch garantiert
einen falschen Fehler aus. Neu: 5s Kulanzfrist fuer die Erstverbindung,
ein bereits gestandener und dann abgebrochener Connect loest weiterhin
sofort aus.
wait_for_fresh_data() nutzte ein einziges Event fuer alle Aufrufer. Bei
zwei Ladepunkten an einem Secondary (duo_num 0/1, gemeinsame Verbindung)
laufen mehrere Aufrufe gleichzeitig - ein Aufrufer konnte dem anderen per
eigenem clear() den Wakeup wegnehmen. Jetzt bekommt jeder Aufruf ein
eigenes Event.
Waiters are notified by messages from unrelated topics
packages/helpermodules/broker.py:106
Every waiter is notified for every topic, not just the prefix it requested. When two external chargepoints share this cached client, a message for duo 1 wakes duo 0's wait, which then returns the accumulated duo-0 values even though no duo-0 message arrived in this cycle; stale readings can therefore be accepted and the no-data path suppressed. Associate each waiter with its topic filter (or otherwise wait only for a matching message).
This issue also appears on line 153 of the same file.
Ignored publish errors can silently lose MQTT messages
packages/helpermodules/broker.py:123
Auch nach der is_connected()-Prüfung kann die Verbindung vor diesem Aufruf abbrechen. In paho-mqtt 1.6.1 liefert Client.publish() dann nur einen Fehlercode (MQTT_ERR_NO_CONN) zurück; der Rückgabewert wird hier ignoriert, sodass Nachrichten verloren gehen, ohne dass ErrorTimerContext den Fehler erkennt. Bitte den rc prüfen und bei jedem nicht erfolgreichen Ergebnis eine Exception auslösen.
Cached MQTT clients leak reconnecting threads
packages/helpermodules/broker.py:171
Clients placed in this process-wide cache are never disconnected or removed. Deleting or changing an external chargepoint creates a new client while the old loop_start() thread continues auto-reconnecting to the obsolete host forever; repeated configuration changes can leak threads and generate needless connection attempts/logs. Add lifecycle cleanup/ref-counting when a host is no longer used.
Retained messages can arrive before waiter registration
subscribe() delivers retained messages asynchronously, but wait_for_fresh_data() registers its waiter only after this call. A retained batch can therefore arrive between these lines, be stored without waking a waiter, and make the first read wait the full 2 seconds before returning {} despite having received the data. Register the waiter before subscribing or make subscription and the first-data wait atomic (for example with a message sequence/generation).
@LKuemmel bevor ich hier weitermache, würd ich gerne deine Meinung bekommen. #3996 soll die Ursache von #3976 beheben (Crash/KeyError, wenn Verbindungsaufbau + Zustellung aller retained Topics nicht ins enge Zeitfenster von control_interval/3 passt). Der Ansatz war eine dauerhafte, automatisch wiederverbindende MQTT-Verbindung statt Neuverbindung pro Zyklus.
Auslöser war eben das ich seit #3976 dauerhaft diese Errors im Log habe:
2026-09-29 21:19:24,563 - {helpermodules.utils._thread_handler:43} - {ERROR:MainThread} - set values cp5 konnte nicht innerhalb des Timeouts abgearbeitet werden.
2026-09-29 21:16:26,214 - {helpermodules.utils._thread_handler:43} - {ERROR:MainThread} - set values cp5 konnte nicht innerhalb des Timeouts abgearbeitet werden.
2026-09-29 21:06:06,465 - {helpermodules.utils._thread_handler:43} - {ERROR:MainThread} - set values cp5 konnte nicht innerhalb des Timeouts abgearbeitet werden.
2026-09-29 21:06:06,462 - {helpermodules.utils._thread_handler:43} - {ERROR:MainThread} - set values cp4 konnte nicht innerhalb des Timeouts abgearbeitet werden.
2026-09-29 21:00:54,400 - {helpermodules.utils._thread_handler:43} - {ERROR:MainThread} - set values cp5 konnte nicht innerhalb des Timeouts abgearbeitet werden.
2026-09-29 21:00:54,256 - {helpermodules.utils._thread_handler:43} - {ERROR:MainThread} - set values cp4 konnte nicht innerhalb des Timeouts abgearbeitet werden.
2026-09-29 20:58:03,796 - {helpermodules.utils._thread_handler:43} - {ERROR:MainThread} - set values cp5 konnte nicht innerhalb des Timeouts abgearbeitet werden.
2026-09-29 20:57:03,636 - {helpermodules.utils._thread_handler:43} - {ERROR:MainThread} - set values cp5 konnte nicht innerhalb des Timeouts abgearbeitet werden.
Die Ursache vermutlich: loadvars.pys _set_values() bündelt alle Geräte/Ladepunkte/Verbraucher in eine gemeinsame Thread-Liste mit einem gemeinsamen Timeout (control_interval/3, bei Default 10s also ~3,33s fürs gesamte System). wait_for_fresh_data() in external_openwb kann dabei by design bis zu ~2,1s pro Zyklus blockieren (2s max_wait + 0,1s settle) - nicht als Fehlerfall, sondern als normales Verhalten, gerade unter der Last/Latenz-Bedingung, die #3976 eigentlich abfangen sollte (einzelne nachtrudelnde Nachrichten setzen den Settle-Timer immer wieder zurück). Das frisst den Großteil des gemeinsamen 3,33s-Budgets fürs ganze System - im Log sieht man auch cp4 (ein interner, komplett unabhängiger Ladepunkt) zum exakt gleichen Zeitpunkt timeouten, was genau dazu passt.
Copilot findet daneben auch mehrfach echte Bugs in dem Ansatz. Das lässt sich zwar jeweils beheben, aber die Komplexität summiert sich, und ich bin nicht mehr sicher, ob der Ansatz in der jetzigen Form das wert ist.
Ein Revert auf die alte Logik würde direkt das alte Problem von oben wieder mit sich bringen. Aber weniger Fehler, könnte man mit leben?
Aber mir fällt auf: fast alle zuletzt gefundenen Bugs von Copilot betreffen konkret das Teilen einer Verbindung zwischen zwei Ladepunkten am selben Host (Duo-Fall), nicht den Grundmechanismus selbst. Würde man stattdessen pro Ladepunkt eine eigene Verbindung halten statt eine pro Host/Port, würde diese ganze Klasse von Bugs von vornherein wegfallen - auf Kosten einer zusätzlichen MQTT-Verbindung pro Duo, was vernachlässigbar sein sollte. Denkbar?
Was denkst du, was ist hier der richtige/gewünschte Weg?
Grundsätzlich halte ich den Ansatz für richtig. Den Code habe ich mir angeschaut, aber noch nicht im Detail auf potenzielle Probleme untersucht.
Neben der Duo gibt es noch weitere Fälle:
AddOn-Platine als IO-Gerät
Wallbox-Steckdose als Verbraucher (für 2.4.0)
Dort wird auch bei jeder Abfrage eine MQTT-Verbindung geöffnet.
Wie willst Du die Verbindungen unterscheiden?
Was mir noch eingefallen ist: Was passiert mit persistenten Verbindungen, wenn die IP geändert wird oder ein Ladepunkte gelöscht wird? Ein grundsätzliches Werkzeug haben wir dafür nicht.
Ehrlich gesagt ist das echt keine kleine Nummer und größer geworden, als ich das allein sauber lösen kann. Mein eigentliches Problem ist der Timeout-Fehler-Spam im Log bei external_openwb (#3976-Folge). Die eigene Verbindung pro Ladepunkt war nur eine Idee von mir, weil mir aufgefallen ist, dass fast alle Copilot-Findings genau den Duo-Fall (geteilte Verbindung) betreffen – kein ausgereiftes Konzept.
AddOn und die Wallbox-Steckdose danke für die Ergänzung zur Duo, aber das ist echt keine kleine Baustelle (Verbindungs-IDs, close()-Hooks für alle Modultypen etc.) – da fühle ich mich nicht sicher genug, das allein richtig zu entwerfen. Und vor allem ist das ja auch schon eine gravierende Änderung. Was wäre denn dein Vorschlag, hast du da eine Idee zu wie man das umsetzen könnte?
Oder vielleicht ne andere Idee zu den permanten Fehlermeldungen, dass man die anders noch abstellen kann?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hintergrund
Auf einem Produktivsystem trat die ganze Nacht wiederholt folgender Fehler für einen externen Ladepunkt (
external_openwb, Secondary per Netzwerk angebunden) auf:Ursache:
get_values()baut pro Regelzyklus 6 einzelne, kurzlebige MQTT-Verbindungen zur Gegenstelle auf (5xpub_single()überpaho.mqtt.publish.single(), das bei jedem Aufruf neu verbindet/trennt, plus 1xBrokerClient(...).start_finite_loop()zum Abonnieren). All das musste bisher gemeinsam in das enge Timeout ausloadvars.pypassen (control_interval/3, bei Standard 10s also ~3,3s) - schon normaler Netzwerk-Jitter zur Gegenstelle konnte das reißen.Das wurde durch #3976 verschärft, nicht verursacht: #3976 hat das interne Timeout von
start_finite_loop()bewusst von 1s auf 2s erhöht, um ein anderes, echtes Problem zu beheben (gelegentlich fehlende retained Topics unter Last). Das war für sich genommen richtig - aber niemand hat das äußere Timeout inloadvars.pyentsprechend mit angepasst. Vor #3976 blieben von den ~3,3s noch ~2,3s Puffer für die 6 Verbindungsaufbauten übrig, nach #3976 nur noch ~1,3s. Damit wurde genau der Fehler, den dieser PR behebt, durch #3976 spürbar wahrscheinlicher.Die Lösung
Statt pro Zyklus neu zu verbinden, hält
PersistentBrokerClient(neu,helpermodules/broker.py) eine einzelne, dauerhafte, automatisch wiederverbindende MQTT-Verbindung pro Gegenstelle offen - wird von allenpub_single()-Aufrufen zu diesem Host genutzt und bleibt durchgehend abonniert, statt pro Zyklus neu zu verbinden/abzumelden. Das ist kein neues Muster: Der komplette Datenkern von openWB (SubData,command.py,setdata.py, der simpleAPI-MQTT-Daemon) funktioniert schon heute nach exakt diesem Prinzip - nur bisher ausschließlich für den lokalen Broker. Neu ist hier lediglich, dieses Prinzip auch für Verbindungen zu einer entfernten Gegenstelle zu nutzen (inkl.reconnect_delay_set, das die bestehenden lokalen Verbindungen nicht brauchen, weil der lokale Broker als praktisch immer verfügbar gilt).Bestehende Sicherheitsmechanismen bleiben erhalten:
publish()wirft jetzt explizit einen Fehler, wenn die Verbindung nicht steht (pahosClient.publish()schlägt bei fehlender Verbindung sonst nur mit einem Rückgabecode fehl, ohne Exception) - sonst hätte die bestehendeclient_error_context-Sicherheitsabschaltung (Soll-Strom nach 60s anhaltendem Fehler auf 0) nie mehr ausgelöst.wait_for_fresh_data()liefert weiterhin ein leeres Dict (nicht die letzten bekannten Werte), wenn in diesem Zyklus nichts Neues angekommen ist - die bestehende "keine Daten empfangen"-Warnung funktioniert dadurch unverändert.OSError-Handler (Die Verbindung zum Host ist fehlgeschlagen. Überprüfe Adresse und Netzwerk.) statt einer nichtssagenden Fallback-Meldung.Getestet
PersistentBrokerClient(broker_test.py, gemocktermqtt.Client, keine echten Sockets): Verbindungsprüfung/Exception, Abo-Deduplizierung, Resubscribe nach Reconnect, Timing-Logik vonwait_for_fresh_data().Was nicht getestet wurde
duo_num0/1, gemeinsame Verbindung) - die Testumgebung hatte nur einen konfiguriert. Sollte durch das Verbindungs-Caching pro Host funktionieren, war aber nicht live zu verifizieren.