🚧 BlockingCall(), Blockierende Funktionen aufrufen, Ping, Shellbefehle, qx()

Begonnen von Torxgewinde, 20 Februar 2025, 11:51:21

Vorheriges Thema - Nächstes Thema

Torxgewinde

Hallo,
Mit BlockingCall() kann man recht einfach Aufrufe machen, die FHEM blockieren würden. Das Ergebnis landet in dem Reading "result".

defmod BlockingCallTest dummy
attr BlockingCallTest readingList wert
attr BlockingCallTest setList wert
attr BlockingCallTest userReadings result:wert:.* {\
    my $wert = ReadingsVal($name, "wert", "???");;\
    \
    if (!defined &BlockingCallTestFunct1) {\
        *BlockingCallTestFunct1 = sub ($) {\
            my ($param) = @_;;\
            my $result;;\
            $result = qx($param);;\
            $result = MIME::Base64::encode_base64($result);;\
            $result =~ s/\n//g;;\
            return $result;;\
        };;\
    }\
    \
    if (!defined &BlockingCallTestFunct2) {\
        *BlockingCallTestFunct2 = sub ($) {\
            my ($result) = @_;;\
            my $hash = $defs{$name};;\
            $result = MIME::Base64::decode_base64($result);;\
            readingsSingleUpdate($hash, $reading, $result, 1);;\
        };;\
    }\
    \
    BlockingCall("BlockingCallTestFunct1", $wert, "BlockingCallTestFunct2", 10);;\
    \
    return "started job...";;\
}

Befehle führt man dann so non-blocking aus:
  • Ein Ping: set BlockingCallTest wert ping -c 4 example.com
  • Ein wenig Shellzeugs: set BlockingCallTest wert date; sleep 3; date

Screenshot wie es aussehen kann:
Du darfst diesen Dateianhang nicht ansehen.

Torxgewinde

Man kann das auch kombinieren mit einem cmdalias:

defmod shellAlias cmdalias shell .* AS set BlockingCallTest wert $EVENT
Dann kann man die Beispiele so Ausführen und die Ergebnisse landen im BlockintCallTest-Device. Wichtig, auf der FHEM Ebene muss man Semikolon verdoppeln!

  • Das Ping Beispiel: shell ping -c 5 example.com
  • Das date/sleep Beispiel: shell date;; sleep 3;; date


Hackstall

Hallo,,

verstanden nur noch eine Frage:

Wie kann ermitteln dass der returnwert verfügbar ist.

Ich sehe im FHEM dass der Returnwert nach einer gewissen Zeit aktualisiert erscheint und
"start jpb ..." dann überschrieben wird.

Wie müsste ich hier in einer Prozedure vorgehen oder wie müssten meine Prozedur aussehen:

sub shellCmd($)
  {
    my ( $cmd ) = @_;
my $retString = "";

    fhem("set BlockingCallTest wert ".$cmd);
$retString=ReadingsVal("BlockingCallTest", "result","");
    sendMsg("LOG", "1", "shellCmd:$cmd return:$retString");
return $retString;
  }

Torxgewinde

Moin @Hackstall,
Über solch eine Funktion alleine wird es nicht gelingen. Die Aktion wird mit dem ersten Teil angestoßen fhem("set BlockingCallTest wert ".$cmd);, aber dann geht der Code ja direkt an der Stelle weiter und du versuchst das Ergebniss, dass erst in einer Weile zur Verfügung steht, schon direkt anzufragen.

Es ist also notwendig die Schritte aufzuteilen, wenn man nicht in deiner Funktion auf das Ergebnis aktiv warten will. Würde man dort warten wollen, würdest du dort blockieren und hättest nichts gegenüber einem einfachem Aufruf von qx() gewonnen.

Erster Schritt: Job anstoßen, das geht ja mit dem set BlockingCallTest wert ...

Zweiter Schritt: Wenn das Ergebniss bekannt ist, darauf reagieren. Da sich ein Reading ändert (das Reading result beim BlockingCallTest-Device), kannst du zum Beispiel ein Notify verwenden, oder auch ein DOIF, oder oder oder.

Als Beispiel mal als Notify:
defmod BlockingCallTestErgebnis notify BlockingCallTest:result:.* {Log(1, "bla bla bla") }
Solch ein Notify reagiert erstmal auf jede Änderung auf welches der Reguläre-Ausdruck passt, also auch darauf wenn das Reading auf "started job..." gesetzt wird und wenn das Ergebniss eintrudelt. Da dich vermutlich nicht interessiert, wann es losgeht, sondern wenn ein Ergebniss kommt, kann man filtern:

Zum Beispiel so:
defmod BlockingCallTestErgebnis notify BlockingCallTest:result:.* {\
return if ('started job...' eq ReadingsVal('BlockingCallTest', 'result', '???'));;\
Log(1, "bla bla bla");; \
}

Torxgewinde

Alternativ, wenn du es lieber kompakt alles in dem Device zusammenfassen möchtest, kann man auch ein Attribut erschafffen, dessen Inhalt als Code ausgeführt wird, wenn ein Ergebnis eintrifft:

defmod BlockingCallTest dummy
attr BlockingCallTest userattr onResult:textField-long
attr BlockingCallTest onResult my $result = ReadingsVal('BlockingCallTest', 'result', '???');;\
\
return if ('started job...' eq $result);;\
\
Log(1, "bla bla bla");;
attr BlockingCallTest readingList wert
attr BlockingCallTest setList wert
attr BlockingCallTest userReadings result:wert:.* {\
    my $wert = ReadingsVal($name, "wert", "???");;\
    \
    if (!defined &BlockingCallTestFunct1) {\
        *BlockingCallTestFunct1 = sub ($) {\
            my ($param) = @_;;\
            my $result;;\
            $result = qx($param);;\
            $result = MIME::Base64::encode_base64($result);;\
            $result =~ s/\n//g;;\
            return $result;;\
        };;\
    }\
    \
    if (!defined &BlockingCallTestFunct21) {\
        *BlockingCallTestFunct21 = sub ($) {\
            my ($result) = @_;;\
            my $hash = $defs{$name};;\
            $result = MIME::Base64::decode_base64($result);;\
            readingsSingleUpdate($hash, $reading, $result, 1);;\
            my $onResult = AttrVal($name, 'onResult', undef);;\
            eval($onResult) if (defined $onResult);;\
        };;\
    }\
    \
    if (!defined &BlockingCallTestFunct31) {\
        *BlockingCallTestFunct31 = sub ($) {\
            my ($result) = @_;;\
            my $hash = $defs{$name};;\
            readingsSingleUpdate($hash, $reading, $result, 1);;\
            my $onResult = AttrVal($name, 'onResult', undef);;\
            eval($onResult) if (defined $onResult);;\
        };;\
    }\
    \
    BlockingCall("BlockingCallTestFunct1", $wert, "BlockingCallTestFunct21", 10, "BlockingCallTestFunct31", "aborted");;\
    \
    return "started job...";;\
}

Dein Perl-Code, der dann ablaufen soll wenn ein Ergebnis eintrifft, gehört dann in das Attribut onResult.

Der Vorteil ist, dass alles kompakt in diesem Device passiert und die Funktion nicht über mehrere Devices verstreut werden würde. Das macht die Pflege übersichtlicher.

Hackstall

Hi vielen Dank für Deine Antwort.

ich weiss nicht ob ich es komplett richtig verstanden habe aber du schlägst vor hier meinen Aktion zu definieren:

attr BlockingCallTest onResult my $result = ReadingsVal('BlockingCallTest', 'result', '???');;\
1) Wie würde der code denn aussehen wenn ein shell script die werte 1,2,3,4 oder 5 zurückgibt und ich für jeden Wert eine andere Aktion hätte.

2) Wie wäre es wenn ich unterschiedliche Shell Scripts ausführen würde mit unterschiedlichen Ergebnissen.

Vielleicht hättest Du ja noch eine Idee?

Danke vielmals

Torxgewinde

Moin,
Naja, die Frage geht dann eher zum programmieren in Perl.

Zu 1):
Angenommen dein Befehl wäre irgendwas, dass 1 bis 5 als Zeichen zurückgibt (Eigentlich geben Befehle Errorlevel zurück, das werten wir hier jetzt mal nicht aus):

Befehl, der ein paar Sekunden braucht und "1" zurückgibt:
set BlockingCallTest wert sleep 2;; echo 1
Befehl, der ein paar Sekunden braucht und "2" zurückgibt:
set BlockingCallTest wert sleep 2;; echo 2
Befehl, der ein paar Sekunden braucht und "3" zurückgibt:
set BlockingCallTest wert sleep 2;; echo 3
Befehl, der ein paar Sekunden braucht und "4" zurückgibt:
set BlockingCallTest wert sleep 2;; echo 4
Befehl, der ein paar Sekunden braucht und "5" zurückgibt:
set BlockingCallTest wert sleep 2;; echo 5

Ganz simpel und ohne großartig Perl Details kann man dann darauf so reagieren:
defmod BlockingCallTest dummy
attr BlockingCallTest userattr onResult:textField-long
attr BlockingCallTest onResult my $result = ReadingsVal('BlockingCallTest', 'result', '???');;\
\
return if ('started job...' eq $result);;\
\
if ( $result =~ /1/ ) {\
    Log(1, "Das Ergebnis beinhaltet eine 1");;\
    return;;\
}\
\
if ( $result =~ /2/ ) {\
    Log(1, "Das Ergebnis beinhaltet eine 2");;\
    return;;\
}\
\
if ( $result =~ /3/ ) {\
    Log(1, "Das Ergebnis beinhaltet eine 3");;\
    return;;\
}\
\
if ( $result eq "4\n" ) {\
    Log(1, "Das Ergebnis ist genau 4 + Newline");;\
    return;;\
}\
\
if ( $result =~ /^5\s*$/ ) {\
    Log(1, "Das Ergebnis startet mit 5 und endet auf optionale Leerzeichen wie \\n\\r etc.");;\
    return;;\
}\
\
Log(1, "Das Ergebnis ist >>$result<<");;
attr BlockingCallTest readingList wert
attr BlockingCallTest setList wert
attr BlockingCallTest userReadings result:wert:.* {\
    my $wert = ReadingsVal($name, "wert", "???");;\
\
    if (!defined &BlockingCallTestFunct1) {\
        *BlockingCallTestFunct1 = sub ($) {\
            my ($param) = @_;;\
            my $result;;\
            $result = qx($param);;\
            $result = MIME::Base64::encode_base64($result);;\
            $result =~ s/\n//g;;\
            return $result;;\
        };;\
    }\
\
    if (!defined &BlockingCallTestFunct21) {\
        *BlockingCallTestFunct21 = sub ($) {\
            my ($result) = @_;;\
            my $hash = $defs{$name};;\
            $result = MIME::Base64::decode_base64($result);;\
            readingsSingleUpdate($hash, $reading, $result, 1);;\
            my $onResult = AttrVal($name, 'onResult', undef);;\
            eval($onResult) if (defined $onResult);;\
        };;\
    }\
    \
    if (!defined &BlockingCallTestFunct31) {\
        *BlockingCallTestFunct31 = sub ($) {\
            my ($result) = @_;;\
            my $hash = $defs{$name};;\
            readingsSingleUpdate($hash, $reading, $result, 1);;\
            my $onResult = AttrVal($name, 'onResult', undef);;\
            eval($onResult) if (defined $onResult);;\
        };;\
    }\
\
    BlockingCall("BlockingCallTestFunct1", $wert, "BlockingCallTestFunct21", 10, "BlockingCallTestFunct31", "aborted");;\
\
    return "started job...";;\
}

Wobei die Abfrage auch auslöst, wenn das Ergebnis "10" wäre und dann würde der Fall für 1 ausgewertet, da dort der reguläre Ausdruck auch matcht. Wenn du auf das Zeichen genau weißt, was das Ergebnis ist, kannst du mit eq vergleichen. Das habe ich mal für den Fall "4" gezeigt. Fall "5" ist da ein wenig flexibler und erlaubt optionale Zeichen wie \n, \r, TAB oder auch Leerzeichen am Ende.

Zu 2):
Wenn man noch wissen will, was der auslösende Befehl war, kann man den abfragen mit:
my $cmd = ReadingsVal('BlockingCallTest', 'wert', '??');

und dann könnte man das in seine Abfrage mit einbauen:
if ($cmd eq "sleep 2; echo 1" && $result =~ /1\s*/ ) { ... }

Ich hoffe das hilft dir bei deinem Problem, denn was du damit genau vorhast kann ich nicht sehen.