MQTT2_Server Retained Bug

Begonnen von Guybrush, 26 August 2026, 19:00:37

Vorheriges Thema - Nächstes Thema

Guybrush

Beim Entwickeln vom MQTT2_Discovery Modul bin ich über einen Bug im MQTT2_Server gestolpert, der dazu führte, dass bei mir kein Aufruf des mqtt2 server devices mehr möglich war. Alles anderen devices gingen, nur dieses hing. Deswegen hab ich gezielt nach dem Fehler gesucht und tatsächlich wohl einen Bug im MQTT2_Server Modul gefunden.

Bei einem MQTT2_SERVER mit aktivem respectRetain und nicht gesetztem global unicodeEncoding werden retained UTF-8-Payloads beim Fhem Start / Wiederherstellen aus dem RETAIN-/.RETAIN-Reading erneut codiert. Aus "Küche" wird nach dem ersten betroffenen Neustart "Küche", danach "Küche" usw. Nach mehreren Restarts war das entsprechend auf enorme längen gestiegen, wodurch sich symptomatisch zeigte, dass der Aufruf des Devices nicht mehr ging.

Die Wiederherstellung erfolgt soweit ich das recherchiert habe beim FHEM-Start: Beim Einlesen des Statefiles ruft FHEM für das gespeicherte Retain-Reading "MQTT2_SERVER_State()" auf. toJSON() hat die ursprünglichen Bytes zu diesem Zeitpunkt noch korrekt gespeichert. json2nameValue erkennt jedoch die von toJSON erzeugten "\u0022"-Escapes und führt bei deaktiviertem unicodeEncoding anschließend Encode::encode("UTF-8", $val) auf dem gesamten, bereits als UTF-8 vorliegenden Payload aus.

Reproduktion:
respectRetain 1, global unicodeEncoding nicht gesetzt und ein externer MQTT-Client.

1. mosquitto_pub -h <fhem-host> -t test/retain-utf8 -r -m '{"name":"Küche"}'
2. FHEM regulär neu starten, sodass das Statefile geschrieben und anschließend wieder eingelesen wird.
3. Das Topic mit einem neuen Subscriber abrufen. Der Name ist nun `Küche`:
   mosquitto_sub -h <fhem-host> -t test/retain-utf8 -C 1
4. Nach dem Neustart ein anderes retained Topic veröffentlichen. Dadurch wird der gesamte bereits wiederhergestellte Retain-Cache erneut ins Reading geschrieben:
   mosquitto_pub -h <fhem-host> -t test/retain-trigger -r -m 1
5. FHEM erneut starten und "test/retain-utf8" wieder abrufen. Der Name ist jetzt "Küche". Weitere solche Zyklen vergrößern die Verfälschung exponentiell.

Das Problem fiel hier erst spät auf, weil ASCII-Payloads unverändert bleiben und regelmäßig publizierte Status-Topics nach einem Neustart wieder mit korrekten Werten überschrieben werden. Statische Discovery-Topics werden dagegen häufig nur beim Start ihres Publishers gesendet und können deshalb viele FHEM-Neustarts durchlaufen. Für den beobachteten Namen mit 4.100 Zeichen waren ungefähr zwölf Wiederherstellungszyklen erforderlich.

Beim Wiederherstellen des internen Retain-Readings sollte ausschließlich die Legacy-Nachcodierung von json2nameValue unterdrückt werden:
diff --git a/FHEM/00_MQTT2_SERVER.pm b/FHEM/00_MQTT2_SERVER.pm
--- a/FHEM/00_MQTT2_SERVER.pm
+++ b/FHEM/00_MQTT2_SERVER.pm
@@ -267,7 +267,14 @@ MQTT2_SERVER_State()
 
   if($name eq "RETAIN" || $name eq ".RETAIN") {
     my $now = gettimeofday;
-    my $ret = json2nameValue($val);
+    my $ret;
+    {
+      # Retained MQTT-Payloads sind bereits Bytefolgen. Deshalb darf die
+      # Legacy-UTF-8-Konvertierung von json2nameValue hier nicht greifen.
+      local $unicodeEncoding = 1;
+      $ret = json2nameValue($val);
+    }
     for my $k (keys %{$ret}) {
       my %h = ( ts=>$now, val=>$ret->{$k} );
       $hash->{retain}{$k} = \%h;


Die globale Einstellung bleibt dabei unverändert. Im konkreten Test ergaben sich für den Namen folgende Bytefolgen:

vorher: 4b c3 bc 63 68 65
bisherige Wiederherstellung: 4b c3 83 c2 bc 63 68 65
mit dem Patch: 4b c3 bc 63 68 65

rudolfkoenig


Guybrush

Danke Rudi,

ich hab bei mir leider immer noch Zeichenfehler:

mosquitto_sub -h 127.0.0.1 -t '#' -v|more
homeassistant/music_player/RINCON_<snip>/sonos/config {"available_commands":["adv-command","clearqueue","command","joingroup","leavegroup","mute","next
","notify","notifytwo","pause","play","playmode","previous","queue","seek","selecttrack","setbass","setbuttonlockstate","setledstate","setnightmode","setavtranspo
rturi","settreble","sleep","speak","speaktwo","stop","switchtoline","switchtoqueue","switchtotv","toggle","unmute","volume","volumedown","volumeup"],"command_topi
c":"sonos/RINCON_<snip>/control","device":{"identifiers":["RINCON_<snip>"],"manufacturer":"Sonos","name":"KÃ�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã
�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�▒
�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�▒
�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�▒
�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�▒
�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â�Ã�Â
usw...

In der MQTT2_SERVER_State dürfte auch die Handhabung von hideRetain=0|1 verdreht sein:

Stelle    hideRetain=0    hideRetain=1
Dokumentation    RETAIN    .RETAIN
MQTT2_SERVER_Attr()    RETAIN    .RETAIN
MQTT2_SERVER_doPublish()    RETAIN    .RETAIN
MQTT2_SERVER_State()    .RETAIN    RETAIN

Das erklärt zwar nicht woher die Falschkodierung bei mir herkommt, aber der Fix aus dem Vorpost war nicht ausreichend.

diff --git a/FHEM/00_MQTT2_SERVER.pm b/FHEM/00_MQTT2_SERVER.pm
--- a/FHEM/00_MQTT2_SERVER.pm
+++ b/FHEM/00_MQTT2_SERVER.pm
@@ -279,7 +279,7 @@ MQTT2_SERVER_State()
      $hash->{retain}{$k} = \%h;
    }
 
-    my $rname = AttrVal($hash->{NAME}, "hideRetain", 0) ? "RETAIN" : ".RETAIN";
+    my $rname = AttrVal($hash->{NAME}, "hideRetain", 0) ? ".RETAIN" : "RETAIN";
    if($name ne $rname) {
      InternalTimer(0, sub {
        $hash->{READINGS}{$rname} = $hash->{READINGS}{$name};