WebSocket via DevIO?

Begonnen von KernSani, 06 April 2020, 07:43:45

Vorheriges Thema - Nächstes Thema

rudolfkoenig

Kann Dir aus Codestudium leider nicht beantworten, ich wuerde das Problem mit Debug-Ausgaben eingrenzen.

justme1968

@rudi: ich glaube die berechnung der länge bei großen nachrichten ist falsch.

zumindest bekomme ich mit der version aus DevIo fehler, negative längen und unvollständige nachrichten.

mit folgender änderung geht es bei mir:--- DevIo.pm (revision 23927)
+++ DevIo.pm (working copy)
@@ -192,7 +192,7 @@
     $i += 2;
   } elsif( $len == 127 ) {
     return "" if(length($data) < 10);
-    $len = unpack( 'q', substr($hash->{".WSBUF"},$i,8) );
+    $len = unpack('N', substr($data, $i+4, 8));
     $i += 8;
   }

das ist auch der gleiche code den ich auch schon plex modul verwendet habe.

der <10 vergleich muss vermutlich auch noch auf < 12 angepasst werden. das habe ich nicht nicht geprüft.
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

rudolfkoenig

Laut https://tools.ietf.org/html/rfc6455#section-5.2 :
ZitatThe length of the "Payload data", in bytes: if 0-125, that is the payload length.  If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length.  If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the most significant bit MUST be 0) are the payload length.
Laut https://perldoc.perl.org/functions/pack :
Zitatq  A signed quad (64-bit) value.
Q  An unsigned quad value.
N  An unsigned long (32-bit) in "network" (big-endian) order.

N mit $i+4 funktioniert in allen praktischen Faellen, ist aber theoretisch falsch.
Ob Q oder q besser ist, ist angesichts der Definition eine (sehr theoretische) Diskussionsfrage.
Was mich verunsichert: bei q/Q steht in perldoc nix von Network-Byte-Order. Nachgelesen:
ZitatIf your system supports the Q pack format, you can use Q> to get big-endian (since Perl 5.9.2):
Habe jetzt q gegen Q> ausgetauscht, in FHEMWEB und in DevIo. Kann das bitte jemand testen, ich habe gerade meine Schwierigkeiten 64k+ an Websocket Daten zu generieren.

Zitatder <10 vergleich muss vermutlich auch noch auf < 12 angepasst werden. das habe ich nicht nicht geprüft.
Wenn ich das ASCII-Bild im o.g. Link richtig interpretiere, dann ist 10 richtig (2+8).

justme1968

das N theoretisch falsch ist bestreite ich garnicht. praktisch funktioniert es aber.

Q> gab es damals als ich das gebaut habe zumindest für die verwendete perl version noch nicht.

schaut aber mit der 281546 byte langen nachricht beim testen eben gut aus und auf jeden fall besser als die alte variante mit q.
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

Torxgewinde

Hallo,
Der Thread ist zwar schon etwas älter, trotzdem hatte ich die Infos aus diesem Thread gebraucht um Websockets via DevIo nutzen und verstehen zu können. Damit es für Andere einfacher wird, gibt es nun eine Wiki-Seite zu Websockets: https://wiki.fhem.de/wiki/Websocket

Beste Grüße

Torxgewinde

Eine kurze Frage: Besteht Interesse den Umgang mit Ping/Pongs und Toten-Websockets zu verbessern, also direkt in DevIO?

Die Ping/Pongs von Websockets werden, soweit ich das sehe, vom DevIO beantwortet, mit DevIo_Ping() kann man auch ein Ping von DevIO auslösen. Wenn ein Websocket-Ping vom Server reinkommt, wird dem DevIo-API-Nutzer der Empfang eines leeren Strings signalisiert. DevIO bearbeitet die Daten als "intern" bzw. Teil des Protkolls und gibt nur einen leeren String an den DevIo-API-Nutzer.

Wäre es nicht besser, wenn die Websocket mit DevIO eine automatische Untätigkeitsüberwachung und Wiederaufbau der Verbindung beherrscht? Nicht immer kann ich mich auf das OS und dessen Sockets verlassen um eine gestörte Verbindung zu erkennen. Deswegen muss ich in dem Beispiel auf die Leeren-Strings bauen und habe mir ein "last_seen" hinzu programmiert und ausgewertet.

Was ich mir vorstellen kann, wäre ein "last_seen"-Timestamp direkt in DevIO zu loggen wenn Daten eintreffen und diesen dann mit einem Timer-Watchdog direkt in DevIO zu prüfen. Hat man zulange nichts mehr von dem Websocket-Server gehört, dann könnte man noch einmal aktiv ein Ping rausschicken, oder direkt die Verbindung als "disconnected" betrachten und dies auch so zu signalisieren. Das wäre doch schon deutlich besser als die bisherige Lösung, nicht?

Ich kann mir helfen, aber es ist schon komisch darauf zu bauen, dass ein Buffer der leer ist, VERMUTLICH auf einen erfolgreichen Ping/Pong hindeutet. Hier ein Code der mit der Vermutung wohl funktioniert (also Verbindung aufbaut wenn 120 Sekunden lang nichts vom Server gekommen ist), aber ich hätte das eigentlich lieber in DevIO als Automatismus gesehen:

defmod NTFY_RECEIVE dummy
attr NTFY_RECEIVE userattr URL
attr NTFY_RECEIVE URL wss:ntfy.sh:443/FreundlichenGruesseAnAlleFHEMNutzer/ws
attr NTFY_RECEIVE alias NTFY_RECEIVE
attr NTFY_RECEIVE devStateIcon opened:general_ok@green:stop disconnected:rc_STOP@red:start
attr NTFY_RECEIVE eventMap /cmd connect:start/cmd disconnect:stop/
attr NTFY_RECEIVE group Experimente
attr NTFY_RECEIVE icon hue_filled_plug
attr NTFY_RECEIVE readingList cmd
attr NTFY_RECEIVE room Experimente
attr NTFY_RECEIVE setList cmd
attr NTFY_RECEIVE userReadings connect:cmd:.connect {\
    my $hash = $defs{$name};;\
    my $devState = DevIo_IsOpen($hash);;\
    return "Device already open" if (defined($devState));;\
    \
    $hash->{DeviceName} = AttrVal($name, "URL", "wss:ntfy.sh:443/FreundlichenGruesseAnAlleFHEMNutzer/ws");;\
    $hash->{header}{'Host'} = 'ntfy.sh';;\
    $hash->{header}{'User-Agent'} = 'FHEM';;\
    \
    $hash->{directReadFn} = sub () {\
        my $hash = $defs{$name};;\
        readingsBeginUpdate($hash);;\
        my $buf = DevIo_SimpleRead($hash);;\
        \
        # track activity, emtpy buffer normally is from ping/pongs\
        readingsBulkUpdate($hash, "last_seen", int(time()*1000));;\
        RemoveInternalTimer($name.'Timeout');;\
        my $timeoutFunction = sub() {\
            my ($arg) = @_;;\
            my $hash = $defs{$name};;\
            my $myCmd = ReadingsVal($name, "cmd", "disconnect");;\
            return if ($myCmd =~ /disconnect|stop/);;\
            \
            Log3($name, 3, "$name: Timeout occured, restarting websocket...");;\
            DevIo_CloseDev($hash);;\
            readingsBeginUpdate($hash);;\
            readingsBulkUpdate($hash, "state", "disconnected");;\
            readingsBulkUpdate($hash, "cmd", "connect", 1);;\
            readingsEndUpdate($hash, 1);;\
        };;\
        InternalTimer(gettimeofday() + 120, $timeoutFunction, $name.'Timeout');;\
        \
        if(!defined($buf)) {\
            DevIo_CloseDev($hash);;\
            readingsBulkUpdate($hash, "last_seen", 0);;\
            $buf = "not_connected";;\
        }\
        \
        # only update our reading if buffer is not empty and looks like it contains a message\
        if ($buf ne "" && \
            $buf =~ /^{.*"event":"message".*}$/) { ## check if buffer looks like JSON with msg\
            \
            # delete all our readings that begin with "ntfy_"\
            foreach my $reading (grep { $_ =~ /^ntfy_.*/ } keys %{$hash->{READINGS}}) {\
                readingsDelete($hash, $reading);;\
            }\
            \
            # parse as JSON, do not trust the input fully, thus sanitize buffer\
            my %res = %{json2nameValue($buf)};; #(https://wiki.fhem.de/wiki/MQTT2_DEVICE_-_Schritt_f%C3%BCr_Schritt#json2nameValue.28.29)\
            foreach my $k (sort keys %res) {\
                # only keep ASCII and a German Characters like Umlaute, sharp-S...\
                my $sanitizedValue = $res{$k} =~ s/[^[:ascii:]äöüÖÄÜß]/_/rg;; # 'r' flag prevents modifying the input string\
                readingsBulkUpdate($hash, "ntfy_".makeReadingName($k), $sanitizedValue);;\
            }\
        }\
        #readingsBulkUpdate($hash, "websocketData", "$buf") if ($buf ne "");;\
        Log3($name, 3, "$name: Rx: >>>$buf<<<") if ($buf ne "");;\
        \
        readingsEndUpdate($hash, 1);;\
    };;\
    \
    DevIo_OpenDev($hash,\
        0,      ## reopen flag\
        undef,  ## initFn, on success\
        sub() { ## callbackFn, on verdict, req. to make it a non-blocking call\
            my ($hash, $error) = @_;;\
            if ($error) {\
                Log(3, "$name: DevIo_OpenDev Callback: connection failed: $error");;\
                \
                my $timerFunction = sub() {\
                    my ($arg) = @_;;\
                    my $hash = $defs{$name};;\
                    my $devState = DevIo_IsOpen($hash);;\
                    readingsSingleUpdate($hash, "cmd", "connect", 1) if (!defined($devState));;\
                };;\
                \
                RemoveInternalTimer($name.'Timer');;\
                my $rwait = int(rand(20)) + 10;;\
                InternalTimer(gettimeofday() + $rwait, $timerFunction, $name.'Timer');;\
                readingsSingleUpdate($hash, "cmd", "reconnect attempt in $rwait seconds", 1);;\
            }\
        }\
    );;\
    \
    readingsBulkUpdate($hash, "state", "connecting...");;\
    return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
disconnect:cmd:.disconnect {\
    my $hash = $defs{$name};;\
    RemoveInternalTimer($name.'Timer');;\
    RemoveInternalTimer($name.'Timeout');;\
    DevIo_CloseDev($hash);;\
    readingsBulkUpdate($hash, "state", "disconnected") if (!defined(DevIo_IsOpen($hash)));;\
    return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
onDisconnect { ## check on each update if the connection is unintentionally broken...\
    my $myState = ReadingsVal($name, "state", "???");;\
    my $myData = ReadingsVal($name, "websocketData", "???");;\
    my $myCmd = ReadingsVal($name, "cmd", "disconnect");;\
    return if ($myState ne "disconnected" and $myData ne "not_connected");;\
    return if ($myCmd =~ /disconnect|stop/);;\
    \
    my $timerFunction = sub() {\
        my ($arg) = @_;;\
        my $hash = $defs{$name};;\
        my $devState = DevIo_IsOpen($hash);;\
        readingsSingleUpdate($hash, "cmd", "connect", 1) if (!defined($devState));;\
    };;\
    \
    RemoveInternalTimer($name.'Timer');;\
    my $rwait = int(rand(20)) + 10;;\
    InternalTimer(gettimeofday() + $rwait, $timerFunction, $name.'Timer');;\
    readingsBulkUpdate($hash, "cmd", "reconnect attempt in $rwait seconds");;\
    \
    return POSIX::strftime("%H:%M:%S",localtime(time()));;\
}
attr NTFY_RECEIVE verbose 1
attr NTFY_RECEIVE webCmd start:stop

Temporär ist es auch auf Cooltux Demo Server: https://demo-fhem.cooltux.net/fhem?detail=NTFY_RECEIVE Daten senden kann man mit der WebApp: https://ntfy.sh/FreundlichenGruesseAnAlleFHEMNutzer