📜 Wenn die UserReadings mal wieder länger werden, Alternative zu 99_myUtils.pm

Begonnen von Torxgewinde, 19 Mai 2023, 17:17:48

Vorheriges Thema - Nächstes Thema

Torxgewinde

Hallo,
Meine UserReadings wurden mal wieder zu lang. Der übliche Weg wäre ja dann eine 99_myUtils.pm anzulegen und die sich wiederholenden Codeanteile dorthin auszulagern. Da ich den Code aber wirklich nur für diese Device brauchte und lieber alles für dieses Device beim Device speichern wollte, habe ich da eine Alternative mit Beispiel:

Hier ist eine Demonstration, wie ihr diese Methode verwenden könnt:

Legt ein Gerät mit dem Namen "meinTestDevice" vom Typ "dummy" an:
defmod meinTestDevice dummy
Um eigene Attribute zu erstellen, benötigt ihr das Attribut "userattr":
attr meinTestDevice userattr Codeblock1 Codeblock2 CodeblockMitFehler
Füllt diese neuen Attribute mit den gewünschten Codeblöcken. Hier sind ein paar Beispiele:
attr meinTestDevice Codeblock1 sub { my ($a, $b) = @_;; return $a * $b;; }
attr meinTestDevice Codeblock2 $bla = $a * $a + $b;; return "MeinTest";;
attr meinTestDevice CodeblockMitFehler kaputt! sub { my ($a, $b) = @_;; return $a * $b;; }

Um den Code komfortabel zu bearbeiten, überschreibt das übliche Widget und verwendet den Codemirror.js Editor (falls installiert):
attr meinTestDevice widgetOverride Codeblock1:textField-long,87 Codeblock2:textField-long CodeblockMitFehler:textField-long
Nun könnt ihr die Codeblöcke nutzen. Hier sind einige UserReadings mit verschiedenen Anwendungsmöglichkeiten:
attr meinTestDevice userReadings test1 {\
    my $a = 30;;\
    my $b = 55;;\
    \
    #Codeblock in Variable holen, hier nur eine Funktion, kein Fehlerhandling\
    my $FunktionAusCodeblock1 = eval(AttrVal($NAME, "Codeblock1", ""));;\
    \
    #Funktion aus der Variablen ausführen, dabei Parameter übergeben und zurückerhalten\
    my $c = $FunktionAusCodeblock1->($a, $b);;\
    \
    return $c;;\
},\
test2 {\
    my $a = 30;;\
    my $b = 55;;\
    \
    #lokale Variablen aus diesem Scope kann man aus dem Codeblock auch erreichen:\
    my $bla = 123;;\
    my $blubb = eval(AttrVal($NAME, "Codeblock2", "") or die("Fehler: $@"));;\
    \
    return "Bla: $bla, Blubb: $blubb";;\
},\
test3 {\
    my $a = 30;;\
    my $b = ReadingsNum($NAME, "wert", 12345);;\
    \
    #Funktion aus der Variablen ausführen, dabei Parameter übergeben und zurückerhalten\
    my $c = eval(AttrVal($NAME, "Codeblock1", ""))->($a, $b);;\
    \
    return $c;;\
},\
test4 {\
    my $a = 30;;\
    my $b = ReadingsNum($NAME, "wert", 12345);;\
    \
    #Fehler kann man auch abfangen oder weiter signalisieren:\
    #my $MeineFunktion = eval(AttrVal($NAME, "CodeblockMitFehler", "")) or die("Wir haben hier ein Problem: $@");;\
    my $MeineFunktion = eval(AttrVal($NAME, "Codeblock1", "")) or die("Wir haben hier ein Problem: $@");;\
    \
    my $c = $MeineFunktion->($a, $b);;\
    \
    return $c;;\
}

Ich hoffe, diese Methode zur Codeorganisation ist auch für euch hilfreich. Sie ermöglicht eine bessere Übersichtlichkeit und spart den Wechsel zwischen verschiedenen Dateien während der Entwicklung.



Hier nochmal, das komplette Device am Stück:
defmod meinTestDevice dummy
attr meinTestDevice userattr Codeblock1 Codeblock2 CodeblockMitFehler
attr meinTestDevice Codeblock1 sub { my ($a, $b) = @_;; return $a * $b;; }
attr meinTestDevice Codeblock2 $bla = $a * $a + $b;; return "MeinTest";;
attr meinTestDevice CodeblockMitFehler kaputt! sub { my ($a, $b) = @_;; return $a * $b;; }
attr meinTestDevice group Experimente
attr meinTestDevice readingList wert
attr meinTestDevice setList wert
attr meinTestDevice userReadings test1 {\
    my $a = 30;;\
    my $b = 55;;\
\
    #Codeblock in Variable holen, hier nur eine Funktion, kein Fehlerhandling\
    my $FunktionAusCodeblock1 = eval(AttrVal($NAME, "Codeblock1", ""));;\
    \
    #Funktion aus der Variablen ausführen, dabei Parameter übergeben und zurückerhalten\
    my $c = $FunktionAusCodeblock1->($a, $b);;\
    \
    return $c;;\
},\
test2 {\
    my $a = 30;;\
    my $b = 55;;\
    \
    #lokale Variablen aus diesem Scope kann man aus dem Codeblock auch erreichen:\
    my $bla = 123;;\
    my $blubb = eval(AttrVal($NAME, "Codeblock2", "") or die("Fehler: $@"));;\
    \
    return "Bla: $bla, Blubb: $blubb";;\
},\
test3 {\
    my $a = 30;;\
    my $b = ReadingsNum($NAME, "wert", 12345);;\
\
    #Funktion aus der Variablen ausführen, dabei Parameter übergeben und zurückerhalten\
    my $c = eval(AttrVal($NAME, "Codeblock1", ""))->($a, $b);;\
    \
    return $c;;\
},\
test4 {\
    my $a = 30;;\
    my $b = ReadingsNum($NAME, "wert", 12345);;\
\
    #Fehler kann man auch abfangen oder weiter signalisieren:\
    #my $MeineFunktion = eval(AttrVal($NAME, "CodeblockMitFehler", "")) or die("Wir haben hier ein Problem: $@");;\
    my $MeineFunktion = eval(AttrVal($NAME, "Codeblock1", "")) or die("Wir haben hier ein Problem: $@");;\
    \
    my $c = $MeineFunktion->($a, $b);;\
    \
    return $c;;\
}
attr meinTestDevice widgetOverride Codeblock1:textField-long,87 Codeblock2:textField-long CodeblockMitFehler:textField-long

betateilchen

-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

Torxgewinde

🧌🐟 Wenn es objektiv Einwände oder Verbesserungen gibt, dann bin ich für Kommentare offen. Und ansonsten bitte an die Netiquette halten.

Beta-User

myUtils werden precompiled => effizienter, schon für sich genommen...

userReadings ohne trigger sind (oft) ebenfalls ineffizienter als mit => kein vorbildlicher Code.

Und "die" ist im FHEM-Kontext ein nogo!
Server: HP-elitedesk@Debian 12, aktuelles FHEM@ConfigDB | CUL_HM (VCCU) | MQTT2: MiLight@ESP-GW, BT@OpenMQTTGw | MySensors: seriell, v.a. 2.3.1@RS485 | ZWave | ZigBee@deCONZ | SIGNALduino | MapleCUN | RHASSPY
svn: u.a MySensors, Weekday-&RandomTimer, Twilight,  div. attrTemplate-files

Torxgewinde

Danke, das ist nachvollziehbar.

Precompile: Für mich überwiegt die Ergonomie alles auf einer Webseite in dem Webinterface zu sehen, an eine Performancegrenze stoße ich bisher nicht.

Trigger: Das stimmt, sofern der Parameter "wert" ein UserReading triggern soll, bitte:

attr meinTestDevice userReadings test1:wert.* {\
...
}

Fehlerhandling mit die(): Warum ist die() so ein NoGo? Im UserReading wird es korrekt abgefangen und erzeugt eine korrekte Fehlermeldung wie bei Tippfehlern im UserReading selbst auch. In FHEM wird die() auch intensiv genutzt. Ist eine ernstgemeinte Frage, da ich es nicht weiß.

Edit #1:
Ich habe wissen wollen, wieviel Overhead eval() so ungefähr denn mit sich bringt und dabei diesen ganz kruden Benchmark gemacht:
defmod meinTestDevice dummy
attr meinTestDevice userattr Codeblock1 Codeblock2 CodeblockMitFehler
attr meinTestDevice Codeblock1 sub { my ($a, $b) = @_;; return $a * $b;; }
attr meinTestDevice Codeblock2 $bla = $a * $a + $b;; return "MeinTest";;
attr meinTestDevice CodeblockMitFehler kaputt! sub { my ($a, $b) = @_;; return $a * $b;; }
attr meinTestDevice group Experimente
attr meinTestDevice readingList wert
attr meinTestDevice setList wert
attr meinTestDevice userReadings test1 {\
my $a = 30;;\
my $b = 55;;\
my $c = 0;;\
\
use Time::HiRes qw(gettimeofday tv_interval);;\
my $iterations = 0;;\
my $start_time = [gettimeofday()];;\
\
while (1) {\
    #Codeblock in Variable holen, hier nur eine Funktion, kein Fehlerhandling\
my $FunktionAusCodeblock1 = eval(AttrVal($NAME, "Codeblock1", ""));;\
#Funktion aus der Variablen ausführen, dabei Parameter übergeben und zurückerhalten\
$c = $FunktionAusCodeblock1->($a, $b);;\
    \
$iterations++;;\
\
    my $elapsed_time = tv_interval($start_time);;\
last if $elapsed_time >= 0.5;;\
}\
return "$c, Iterations: $iterations";;\
},\
test2 {\
my $a = 30;;\
my $b = 55;;\
my $c = 0;;\
\
use Time::HiRes qw(gettimeofday tv_interval);;\
my $iterations = 0;;\
my $start_time = [gettimeofday()];;\
\
#Codeblock in Variable holen, hier nur eine Funktion, kein Fehlerhandling\
my $FunktionAusCodeblock1 = eval(AttrVal($NAME, "Codeblock1", ""));;\
\
while (1) {\
#Funktion aus der Variablen ausführen, dabei Parameter übergeben und zurückerhalten\
$c = $FunktionAusCodeblock1->($a, $b);;\
    \
$iterations++;;\
\
    my $elapsed_time = tv_interval($start_time);;\
last if $elapsed_time >= 0.5;;\
}\
return "$c, Iterations: $iterations";;\
},\
test3 {\
my $a = 30;;\
my $b = 55;;\
my $c = 0;;\
\
use Time::HiRes qw(gettimeofday tv_interval);;\
my $iterations = 0;;\
my $start_time = [gettimeofday()];;\
\
while (1) {\
#Funcktion in Variable holen, hier diesmal ohne Eval um zu sehen wieviel schneller das ist\
my $FunktionAusCodeblock1 = sub { my ($a, $b) = @_;; return $a * $b;; };;\
\
#Funktion aus der Variablen ausführen, dabei Parameter übergeben und zurückerhalten\
$c = $FunktionAusCodeblock1->($a, $b);;\
    \
$iterations++;;\
\
    my $elapsed_time = tv_interval($start_time);;\
last if $elapsed_time >= 0.5;;\
}\
return "$c, Iterations: $iterations";;\
},\
test4 {\
my $a = 30;;\
my $b = 55;;\
my $c = 0;;\
\
use Time::HiRes qw(gettimeofday tv_interval);;\
my $iterations = 0;;\
my $start_time = [gettimeofday()];;\
\
#Funcktion in Variable holen, hier diesmal ohne Eval um zu sehen wieviel schneller das ist\
my $FunktionAusCodeblock1 = sub { my ($a, $b) = @_;; return $a * $b;; };;\
\
while (1) {\
#Funktion aus der Variablen ausführen, dabei Parameter übergeben und zurückerhalten\
$c = $FunktionAusCodeblock1->($a, $b);;\
    \
$iterations++;;\
\
    my $elapsed_time = tv_interval($start_time);;\
last if $elapsed_time >= 0.5;;\
}\
return "$c, Iterations: $iterations";;\
}
attr meinTestDevice widgetOverride Codeblock1:textField-long,87 Codeblock2:textField-long CodeblockMitFehler:textField-long

Ergebnis:
setstate meinTestDevice 2023-05-20 20:39:59 test1 1650, Iterations: 1633
setstate meinTestDevice 2023-05-20 20:39:59 test2 1650, Iterations: 28064
setstate meinTestDevice 2023-05-20 20:39:59 test3 1650, Iterations: 25151
setstate meinTestDevice 2023-05-20 20:39:59 test4 1650, Iterations: 30022
setstate meinTestDevice 2023-05-20 20:39:59 wert 44

Man sieht also, dass es zeitaufwendig ist, den Codeblock mit eval() innerhalb der Schleife auszuführen. Es ist etwa zehn bis 20 Mal langsamer als die Verwendung einer lokal definierten Funktion. Wenn die Funktion einmal mit eval() ausgeführt wurde und dann nur wiederholt aufgerufen wird, ist die Leistung praktisch identisch mit einer lokalen Funktion. Wenn man also tatsächlich auf Performanceprobleme stoßen sollte, kann man sich das genauer ansehen. Bei den normalerweise verwendeten Frequenzen zur Aktualisierung von Readings ist dies jedoch meiner Meinung nach kein alltägliches Problem, zumindest nicht bei mir, da hier Readings maximal einmal pro Sekunde aktualisiert werden. Daher fällt die teure Ausführung von eval() praktisch nicht ins Gewicht.

Beta-User

Wg. "die()" zitiere ich einfach mal:
Zitat von: rudolfkoenig am 10 Mai 2022, 13:33:24Sorry, ich habe das letze Mal nicht gruendlich genug gelesen.
- lsof ist nur dann sinnvoll, wenn das Problem vorhanden ist..
- da bei der Fehlermeldung WOL die() Aufruft, ist das schlecht praktikabel.

die() ist in einem FHEM-Modul Tabu => der Maintainer von WOL sollte das bitte entfernen.
Erst danach hat man die Moeglichkeit lsof im Problemfall aufzurufen.
die() ist mAn. auch dann "Tabu", wenn man es (indirekt per userReadings) in "eval" einpackt. Es gibt mAn. bessere Varianten, wie man mit Fehlern umgeht - eine wäre z.B. zu finden in "Perl für FHEM-User" irgendwo zum Thema json auspacken...

Just my2ct zu "eval fällt nicht ins Gewicht":
Es ist einfach kein gutes Coding, das so zu lösen, da es a) offenkundig ineffizient ist und b) nicht nur "einmalig" stattfindet, sondern bei jedem Aufruf (und zwar doppelt, weil die Ausführung in FHEM selbst auch nochmal in ein String-eval verpackt wird, was man aber nicht verhindern kann) und v.a.:
ich finde die Begründung "Ergonomie" auch nicht nachvollziehbar! Man bearbeitet solche Funktionen (so man denn wirklich userReadings braucht, was auch in vielen Fällen unter "wer braucht das?!?" fällt) nicht jeden Tag und hat dann auch DIREKT beim Abspeichern der myUtils-File eine Rückmeldung, ob (zumindest) die Syntax korrekt ist.

Aber jedem das seine.
Für mich jedenfalls auch kein "echter" Kandidat für "Tipp der Woche", sondern eher ein nicht nachahmenswertes Beispiel von "so was braucht keiner".

Aber wie gesagt: nur meine 2ct.

PS: Es gibt nicht allzuviele "echte" Treffer beim grep nach die() im FHEM-Verzeichnis, aber falls der eine oder andere Maintainer mitliest, kann er ja mal schauen, ob was dabei ist, was im Sinne obiger Äußerung von Rudi überdenkenswert wäre...
Server: HP-elitedesk@Debian 12, aktuelles FHEM@ConfigDB | CUL_HM (VCCU) | MQTT2: MiLight@ESP-GW, BT@OpenMQTTGw | MySensors: seriell, v.a. 2.3.1@RS485 | ZWave | ZigBee@deCONZ | SIGNALduino | MapleCUN | RHASSPY
svn: u.a MySensors, Weekday-&RandomTimer, Twilight,  div. attrTemplate-files

Torxgewinde

Die Bedenken wirken dann doch sehr bemüht und konstruiert.

Das Fehlerhandling mit die() ist in den Beispielen mit drin, es ist hilfreich und optional um an der richtigen Stelle Fehler angezeigt zu bekommen. Wenn man gegen die() so eine Aversion hat, kann man es ja als optional ansehen und auch weglassen. So wie hier gezeigt, wird die() nicht die restliche Funktion von FHEM stören oder gar das fhem.pl Skript beenden.

Die Bedenken zur Performance wären relevant, falls man denn die UserReadings in hoher Frequenz ausführt, also hundertemale pro Sekunde. Generell ist Eval() eine relativ teure Funktion (siehe Benchmark oben), aber um seine UserReadings aufzuräumen ist es angebracht und akzeptabel, ja sogar nützlich. Ob die Berechnung eines UserReading nun 1ms oder 10ms brauchen sollte ist doch wirklich nicht praxisrelevant. Skaliert man das Problem, ist immer noch Platz für viele derartige Readings.

Beta-User

Zitat von: Torxgewinde am 23 Mai 2023, 07:45:36Die Bedenken wirken dann doch sehr bemüht und konstruiert.
Nun ja, wer sich die Mühe macht, mal Rudi's Beiträge zum Thema "die()" zu suchen, wird jedenfalls feststellen, dass das (ziemlich willkürlich gewählte) Zitat keinesfalls ein Einzelfall ist.
 
Dass hier in dem gezeigten Beispiel die() sauber verpackt war, hat ja niemand in Abrede gestellt. Ich steh' nun mal auf einfache Regeln, und die heißt zu "die()" eben zumindest nach Rudi's und meiner Ansicht: Hat im FHEM-Kontext nichts verloren, kann man anders lösen (Wer $@ unbedingt füllen will kann mit Carp::Carp arbeiten, z.B.).

Ansonsten ist alles gesagt, mag sich jeder seine Meinung dazu bilden.
Server: HP-elitedesk@Debian 12, aktuelles FHEM@ConfigDB | CUL_HM (VCCU) | MQTT2: MiLight@ESP-GW, BT@OpenMQTTGw | MySensors: seriell, v.a. 2.3.1@RS485 | ZWave | ZigBee@deCONZ | SIGNALduino | MapleCUN | RHASSPY
svn: u.a MySensors, Weekday-&RandomTimer, Twilight,  div. attrTemplate-files

marvin78

Zitat von: Torxgewinde am 23 Mai 2023, 07:45:36Die Bedenken wirken dann doch sehr bemüht und konstruiert.


Tatsächlich habe ich genau das über deine Begründung für diese Vorgehensweise gedacht.

Desweiteren hat Beta-User alles gesagt bzw. war schon der Beitrag von betateilchen mehr als ausreichend :)

Torxgewinde

Ich filter' mal das Unsachliche weg:
  • Aufgrund der möglichen Fehler und bereits gemachten Erfahrungen wird von die() abgeraten, auch wenn es hier korrekt verwendet wurde.
  • Das UserReading wird durch eval() interpretiert, ein zweites eval() in dem Code wäre laut @Beta-User ineffizient.

Objektiv gesehen handelt es sich um Laufzeitunterschiede von wenigen Millisekunden bis zu einigen Millisekunden mehr, während die Pausen zwischen den Reading-Updates mehrere tausend Millisekunden betragen. Dies ist der 'Preis', den man zahlt, um die Konfiguration auf der Detailseite des Geräts schön kompakt zu halten. Ich sehe darin durchaus eine Ergonomie und einen sinnvollen Kompromiss - da kommen wir nicht überein - ist dann eben so.

marvin78

Es stehen also messbare Werte einem gefühlten Ergonomiegewinn gegenüber (der im Übrigen durchaus diskutabel ist - ich sehe den nämlich ganz und gar nicht). Jeder so, wie er mag...

betateilchen

-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

Torxgewinde

Um weiteres Trolling zu unterbinden mach' ich denk Thread zu.

ZitatIn slang, a troll is a person who posts or makes inflammatory, insincere, digressive, extraneous, or off-topic messages online (such as in social media, a newsgroup, a forum, a chat room, an online video game), or in real life, with the intent of provoking others into displaying emotional responses, or manipulating others' perception, thus acting as a bully or a provocateur. The behavior is typically for the troll's amusement, or to achieve a specific result such as disrupting a rival's online activities or purposefully causing confusion or harm to other people.