GELÖST: Status und Status-Icon eines Gerätes ändern, OHNE die Aktion auszulösen?

Begonnen von Uli Zappe, 06 Juli 2016, 05:00:12

Vorheriges Thema - Nächstes Thema

Uli Zappe

So, ich habe jetzt beide vorgeschlagenen Lösungsansätze getestet und will wie versprochen des Ergebnis berichten.

Kurz gefasst ist das Ergebnis erfreulicherweise: Beide Ansätze funktionieren gleich gut; es ist also mehr oder weniger Geschmacksache, welchen Ansatz man bevorzugt. :) Einen kleinen Unterschied gibt es; den erläutere ich im abschließenden Abschnitt Zusammenfassung.

Im Folgenden der jeweilige Code, der sich auf die für die Problemstellung entscheidenden Konfigurationseinträge beschränkt. Ebenso habe ich auf kompliziertere Dinge wie Perl-Subroutinen verzichtet und der Anschaulichkeit halber alles in fhem.cfg konfiguriert.

Zur Erinnerung: Die Problemstellung war exemplarisch, dass der aktuelle Status (wach/schlafengelegt) des Computers woodstock mit Hilfe einer sekündlichen ping-Abfrage des Moduls PRESENCE angezeigt wird und sich auch ändern lässt (also woodstock aufwecken oder schlafenlegen lässt), ohne dass eine bloße Änderung der Statusanzeige (weil woodstock von anderer Seite aufgeweckt oder schlafengelegt wurde) unnötig (und möglicherweise Komplikationen verursachend) die Weck- und Schlafenleg-Aktionen auslöst.

Im Beispiel gibt es zwei Kommandozeilenprogramme, mit denen Computer geweckt und schlafengelegt werden kann: wake und sleepmac, die sich beide in /usr/local/bin/ befinden und 1 Argument benötigen, den Hostnamen des Computers (also eben woodstock im Beispiel).

Lösung 1: Der power-Befehl in PRESENCE

Das Modul PRESENCE lässt sich nicht nur zum Feststellen des aktuellen Status von woodstock mittels ping verwenden, sondern kann auch gleich selbst zum Aufwecken und Schlafenlegen verwendet werden, indem man für das Attribut powerCmd einen entsprechenden Befehl festlegt. Die PRESENCE-Instanz sollte also gleichzeitig das auf der Web-Oberfläche sichtbare Gerät sein.

Der Code sieht so aus:


define woodstock_SWITCH dummy
attr woodstock_SWITCH setList on off
define woodstock_SWITCHon notify woodstock_SWITCH:on "/usr/local/bin/wake woodstock"
define woodstock_SWITCHoff notify woodstock_SWITCH:off "/usr/local/bin/sleepmac woodstock"

define woodstock_STATE PRESENCE lan-ping woodstock 1 1
attr woodstock_STATE ping_count 1
attr woodstock_STATE devStateIcon present:on:off absent:off:on
attr woodstock_STATE eventMap /power on:on/power off:off/
attr woodstock_STATE powerCmd set woodstock_SWITCH $ARGUMENT
attr woodstock_STATE event-on-change-reading state


Mit Erläuterungen:

# Da wir für on (aufwecken) und off (schlafenlegen) zwei ganz unterschiedliche Kommandozeilenprogramme auslösen
# müssen, benötigen wir einen dummy und zwei notify-Befehle, um diese Zuordnung vorzunehmen

define woodstock_SWITCH dummy
attr woodstock_SWITCH setList on off
define woodstock_SWITCHon notify woodstock_SWITCH:on "/usr/local/bin/wake woodstock"
define woodstock_SWITCHoff notify woodstock_SWITCH:off "/usr/local/bin/sleepmac woodstock"


# Definition der PRESENCE-Instanz für ping-Abfragen im Sekundenabstand
define woodstock_STATE PRESENCE lan-ping woodstock 1 1
# Bei sekündlichen Abfragen können wir nur einen ping pro Abfrage nutzen; sollte der einmal nicht funktionieren, ist das
# angesichts der sekündlichen Abfragen kein Problem

attr woodstock_STATE ping_count 1
# Das folgende Attribut legt fest, dass auf der Web-Oberfläche für den Status present ein on-Icon und für den Status absent
# ein off-Icon angezeigt wird und ein Klicken auf das Icon den jeweils entgegengesetzten Befehl (on > off, off > on) auslöst

attr woodstock_STATE devStateIcon present:on:off absent:off:on
# Das folgende Attribut legt fest, dass "set woodstock_STATE on|off" "set woodstock_STATE power on|off" auslöst
attr woodstock_STATE eventMap /power on:on/power off:off/
# Das folgende Attribut legt fest, dass "set woodstock_STATE power on|off" "set woodstock_SWITCH on|off" auslöst
attr woodstock_STATE powerCmd set woodstock_SWITCH $ARGUMENT
# Das folgende Attribut verhindert Updates der Weboberfläche im Sekundenabstand, wenn der Status unverändert bleibt.
# Ohne dieses Attribut würde das Status-Icon auf der Weboberfläche im Sekundenabstand flackern.

attr woodstock_STATE event-on-change-reading state


Lösung 2: Verdopplung der Status im Geräte-dummy

Wenn man im auf der Web-Oberfläche sichtbaren Geräte-dummy, der den Computer repräsentiert, die Status von on off auf on ON off OFF verdoppelt, das Status-Icon allen Status zuweist, die Aktionen aber nur für on und off auslöst, kann man Status-Anzeige und Aktionen entkoppeln:

define woodstock dummy
attr woodstock setList on off ON OFF
attr woodstock devStateIcon on:on:off ON:on:off off:off:on OFF:off:on
define woodstockOn notify woodstock:on "/usr/local/bin/wake woodstock"
define woodstockOff notify woodstock:off "/usr/local/bin/sleepmac woodstock"

define woodstock_STATE PRESENCE lan-ping woodstock 1 1
attr woodstock_STATE ping_count 1
define woodstock_PAIRon notify woodstock_STATE:present { if(Value("woodstock") eq "OFF" || Value("woodstock") eq "off") { fhem("set woodstock ON") } }
define woodstock_PAIRoff notify woodstock_STATE:absent { if(Value("woodstock") eq "ON" || Value("woodstock") eq "on") { fhem("set woodstock OFF") } }


Mit Erläuterungen:

# dummy, der den Computer auf der Web-Oberfläche repräsentiert
define woodstock dummy
# verdoppelte Status
attr woodstock setList on off ON OFF
# Das Status-Icon reagiert jeweils gleich auf on und ON sowie auf off und OFF;
# die bei Klick auf das Icon gesendeten Befehle sind aber immer on und off

attr woodstock devStateIcon on:on:off ON:on:off off:off:on OFF:off:on

# Kopplung von on und off des dummys an die entsprechenden Kommandozeilenprogramme
define woodstockOn notify woodstock:on "/usr/local/bin/wake woodstock"
define woodstockOff notify woodstock:off "/usr/local/bin/sleepmac woodstock"


# Definition der PRESENCE-Instanz für ping-Abfragen im Sekundenabstand
define woodstock_STATE PRESENCE lan-ping woodstock 1 1
# Bei sekündlichen Abfragen können wir nur einen ping pro Abfrage nutzen; sollte der einmal nicht funktionieren, ist das
# angesichts der sekündlichen Abfragen kein Problem

attr woodstock_STATE ping_count 1

# Kopplung des Status von woodstock_STATE an den dummy woodstock (Updates werden nur bei verändertem Status gesendet)
define woodstock_PAIRon notify woodstock_STATE:present { if(Value("woodstock") eq "OFF" || Value("woodstock") eq "off") { fhem("set woodstock ON") } }
define woodstock_PAIRoff notify woodstock_STATE:absent { if(Value("woodstock") eq "ON" || Value("woodstock") eq "on") { fhem("set woodstock OFF") } }



Zusammenfassung

Die Lösungen scheinen ziemlich gleichwertig zu sein; sie arbeiten, so weit ich sehen kann, gleich stabil, und sie haben etwa die gleiche Anzahl von Zeilen in fhem.cfg (die 2. Lösung ist eine Zeile kürzer).

Einen kleinen Unterschied im Verhalten gibt es aber, der sich nur dann bemerkbar macht, wenn zwischen einem Klick auf das Status-Icon und dem Zeitpunkt, wo der erstrebte Status erreicht wird, einige Zeit (ein paar Sekunden) vergehen. Typischerweise ist das beim Aufwecken eines Computers der Fall; bis er wieder auf pings reagiert, können einige Sekunden verstreichen (der Ruhezustand hingegen tritt zumindest bei Macs ohne Zeitverzögerung ein).

Das PRESENCE-Modul ist nun so programmiert, dass sich das Status-Icon erst ändert, wenn der Zielzustand erreicht ist. Klicke ich also, wenn der Computer auf off steht, auf on, so bleibt das Status-Icon zunächst unverändert auf off. Erst wenn der Computer wirklich vollständig aufgewacht ist (= auf pings reagiert), wechselt das Status-Icon auf on.

Die 2. Lösung verhält sich hier anders. Ein Klick auf on im dummy setzt dessen Status-Icon sofort auf on; macht woodstock_STATE sein nächstes Statusupdate, wird das Status-Icon zunächst wieder auf off gesetzt, bis es schließlich erneut auf on springt, wenn der Computer vollständig aufgewacht ist.

Mit anderen Worten: Die 1. Lösung verhält sich ,,gelassener"; das Status-Icon ändert sich erst, wenn der Zielstatus erreicht ist. Dafür erhält man keinerlei Feedback, ob man tatsächlich geklickt hat. Die 2. Lösung bietet dieses Feedback, weil das Status-Icon kurzzeitig auf on springt, bevor es zunächst wieder auf off zurückfällt; dafür wirkt dieses Verhalten unruhiger. Letztlich ist das eine Geschmacksfrage; und man kann sich anhand ihrer ja die genehme Lösung aussuchen.

marvin78

Die beste Lösung ist jedoch Möglichkeit 3: ein einziges PRESENCE Device, das, wie beschrieben, ohne zusätzlichen dummy und notify auskommt.

Uli Zappe

Zitat von: marvin78 am 08 Juli 2016, 07:35:22
Die beste Lösung ist jedoch Möglichkeit 3: ein einziges PRESENCE Device, das, wie beschrieben, ohne zusätzlichen dummy und notify auskommt.

Nein, unter den eingangs explizit aufgelisteten Vorraussetzungen für die geschilderten Lösungen ...
Zitat von: Uli Zappe am 08 Juli 2016, 05:13:35
Ebenso habe ich auf kompliziertere Dinge wie Perl-Subroutinen verzichtet
... gibt es diese dritte Möglichkeit nicht.

Warum sie besser sein sollte als die 1. Lösung, wird auch Dein Geheimnis bleiben. Ob man nun einen Dummy oder eine Perl-Subroutine bevorzugt, ist wohl eher Geschmackssache. Und da sich so ein detailliert geschilderter Lösungsvorschlag eher an Einsteiger richtet, fand ich die Lösung ohne Perl-Subroutine die geeignetere. Wer mit Perl-Subroutinen umgehen kann, wird kein Problem damit haben, meinen Dummy durch die von Dir ja zuvor geschilderte Lösung zu ersetzen.

marvin78

Lösungen, die auf weniger Devices setzen (die ggf. gepflegt werden müssen) sind als effektiver und einfacher wartbar zu bezeichen. Das besagt schon die Logik. Zu erwähnen ist die "Ein-Device-Lösung" hier in jedem Fall.

Benni

Außerdem ist jedes zusätzliche notify auch eine zusätzliche (Performance-)Belastung für das System.

justme1968

schön das du deine beiden varianten so ausführlich vorstellst.

zu ein device ist besser:
es gibt in fhem einige stellen an denen die komplette device liste durchgegangen werden muss um bestimmte dinge zu tun. sehr viele davon sind zwar inzwischen optimiert. und es macht tatsächlich auf fast keiner platform mehr einen unterschied ob man ein device mehr hat oder nicht. du darfst aber nicht vergessen das auch notifys als device zählen. in deinem beispiel wird also nicht nur ein zusätzlicher dummy verwendet sondern insgesamt sind es pro presence drei zusätzliche devices bzw. insgesamt vier mal so viele. wenn man das überall macht summiert es sich sehr schnell.

für jeden (auch für dich) wird es in ein paar tagen oder wochen einfacher sein ein einziges device anzuschauen und nachzuvollziehen als vier die miteinander verknüpft sind.

es sind übrigens keine komplizierten perl subroutinen nötig. du kannst entweder der perl code direkt im powerCmd attribut angeben oder du kannst deine beiden wake und sleep programme (z.b. per shellscript) zu einem zusammen fassen und beim aufruf den aktuellen zustand mit übergeben.

spätestens wenn du mehr als ein gerät auf diese art einbinden willst und die ein device variante den beiden eben vorstellten gegenüber stellst sollte ersichtlich sein wie viel weniger zeilen implementierung und weniger duplizierten code du damit hast.

gruss
  andre
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

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

Uli Zappe

Zitat von: justme1968 am 08 Juli 2016, 10:40:46
zu ein device ist besser:
[... gute Argumente pro 1 device ...]
Das ist historisch ganz witzig:

Die Lösung, die ich jetzt endlich endgültig angegangen bin, schwebte mir schon seit 2012 vor, als ich mit FHEM begann. Damals gab es PRESENCE noch nicht, und ich habe daher im Forum zur Diskussion gestellt, ob es in FHEM ein Modul für Geräte wie Computer etc. geben sollte, die besondere Ein-/Ausschaltkommandos haben und deren Status durch sowas wie pings regelmäßig überprüft werden muss.

Rudis Antwort war damals sinngemäß, ich würde zu monolithisch denken. FHEM sei aber ein modulares System, und viel besser als ein solch ,,großes", zu spezialisiertes Device seien mehrere kleinere, die durch notify verbunden werden. Das habe ich seitdem zu beherzigen versucht.  ;)

Generell finde ich, bei der heute verfügbaren Rechenleistung spielt der Anspruch an Systemressourcen eine untergeordnete Rolle. Wobei ja sogar auf meinem 400-MHz-ARM-FHEM-Rechnerchen mit gerade mal 64 (!) MB RAM jetzt 5 PRESENCE-Instanzen mit Abfragen im Sekundentakt samt notifys und dummys völlig problemlos laufen. Da sind Fragen wie Code-Verständlichkeit IMHO wichtiger, und was man verständlicher findet, ist zu einem gewissen Grad dann wieder subjektiv.

Wie auch immer, für mich selbst stellt sich diese Frage jetzt ohnehin nicht, da ich mich nach einem Tag ausgiebiger Tests jetzt für Lösung 2 entschieden habe, da mir das unmittelbare visuelle Feedback, dass der Befehl getriggert wurde, wichtiger ist, als ich vermutet hätte. Aber wenn man mehrere Sekunden warten muss, bis das Status-Icon umspringt, und man bis dahin einfach nicht weiß, ob man mit seinem Wurstfinger auf dem Smartphone überhaupt den Schalter getroffen hat, kann das ziemlich irritieren.

justme1968

nur der vollständigkeit halber: auch das geht mit einem einzigen device :)

gruss
  andre
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

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

Uli Zappe


Uli Zappe

Nachdem sich hier mehrere Forumsteilnehmer für eine Lösung mit nur einem FHEM-Gerät stärkt gemacht haben und bei einer vollumfänglichen Implementierung auch noch Logging-Probleme auftauchen, die am besten in einer Perl-Subroutine zu behandeln sind, möchte ich abschließend noch eine etwas ausgefeiltere Lösung vorstellen, die an Lösung 1 anknüpft, aber mit solch einer Perl-Subroutine arbeitet. Sie löst das Logging-Problem und erlaubt auch, ein GUI-Feedback einzuschalten, wie es oben nur Lösung 2 bot.

Zunächst zu dem
Logging-Problem:

Anders als etwa das Ein- und Ausschalten einer Lampe ist das Aufwecken und Schlafenlegen z.B. eines Computers keine ganz triviale Angelegenheit, weswegen ich gerne auch die erfolgreiche Befehlsausführung in der globalen FHEM-Logdatei protokolliert haben wollte.

Nun gibt es in FHEM aber leider einen seltsamen Bug oder jedenfalls ein ungelöstes Problem: Der Rückgabewert von Shell- bzw. Kommandozeilenbefehlen lässt sich in Perl nicht ermitteln; sowohl system() als auch Backquotes geben in Kontrast zu korrektem Verhalten immer den Wert -1 zurück, ganz egal, wie der tatsächliche Rückgabewert lautete.

Zudem sind Fehlermeldungen der Kommandozeile nur dann in Perl verfügbar, wenn sie auf stdout geschrieben wurden; d.h. stderr muss stets auf stdout umgeleitet werden. Die einzige Möglichkeit, dann noch Erfolgs- von Fehlermeldungen zu unterscheiden, ist, Erfolgsmeldungen zu unterdrücken (= Umleitung von stdout auf /dev/null), so dass ein leerer String als Rückgabewert mit Erfolg gleichzusetzen ist; in Perl muss die Erfolgsmeldung dann neu erzeugt werden.

Das ist die Methode, die die folgende Perl-Subroutine mit

Kommandozeilenbefehl 2>&1 1>/dev/null

anwendet.


Die Perl-Subroutine powerCommand()

Ich habe versucht, eine – jedenfalls im Rahmen meines Anwendungsspektrums – möglichst universell einsetzbare Subroutine zu stricken. Einige Voraussetzungen in Bezug auf die Shellskripte oder Kommandozeilenbefehle, die die eigentlichen Aktionen auslösen, gibt es aber:

  • Die Befehle reagieren entweder auf die Parameter on und off, oder es gibt zu Aufwecken und Schlafenlegen zwei getrennte Befehle.
  • Die Befehle benötigen als Parameter entweder den (auflösbaren) Hostnamen bzw. die IP-Adresse des zu kontrollierenden Gerätes im Netzwerk, oder sie benötigen gar keinen solchen Parameter (weil es nur ein entsprechendes Gerät gibt, dessen IP-Adresse fest in den Befehl eincodiert ist)
  • Werden sowohl Hostname als auch Ein-/Aus-Parameter benötigt, ist die Parameter-Reihenfolge befehl hostname on|off
  • Die Befehle liegen, wie für nutzerspezifische Befehle üblich, in /usr/local/bin/. Wird zum Aufwecken z.B. /usr/bin/wakeonlan verwendet, so hilft ein Link nach /usr/local/bin/.

Die Subroutine hat die folgenden 7 Parameter:

presenceName, hostname, action, command, onCommand, offCommand, guiFeedback

  • presenceName Der Name der PRESENCE-Instanz, als deren powerCmd die Subroutine gesetzt wird. Steht in PRESENCE als Variable $NAME zur Verfügung. Wenn der Name der PRESENCE-Instanz identisch ist mit dem Hostnamen des zu kontrollierenden Gerätes, kann auch ein leerer String übergeben werden.
  • hostname Der (auflösbare) Hostname bzw. die IP-Adresse des zu kontrollierenden Gerätes, oder ein leerer String, falls der/die Kommandozeilenbefehl(e) diese Angabe nicht benötigen
  • action on oder off. Steht in PRESENCE als Variable $ARGUMENT zur Verfügung.
  • command Der Kommandozeilenbefehl (ohne Pfad), der unter Auswertung der Parameter on oder off das zu kontrollierende Gerät aufweckt oder schlafenlegt, oder ein leerer String, falls für Aufwecken und Schlafenlegen zwei unterschiedliche Befehle benötigt werden.
  • onCommand Der Kommandozeilenbefehl (ohne Pfad), der das zu kontrollierende Gerät aufweckt, oder ein leerer String, falls hierfür ein kombinierter Befehl mit den Parametern on und off verwendet wird
  • offCommand Der Kommandozeilenbefehl (ohne Pfad), der das zu kontrollierende Gerät schlafenlegt, oder ein leerer String, falls hierfür ein kombinierter Befehl mit den Parametern on und off verwendet wird
  • guiFeedback Schaltet das GUI-Feedback aus (0) oder ein (jeder andere Wert). Ist das GUI-Feedback eingeschaltet, wechselt das Schaltflächen-Icon bei Betätigung kurz in den Zielzustand, um die Auslösung de Befehls zu signalisieren, bevor das Icon wieder den Ist-Zustand anzeigt, bis der Zielzustand tatsächlich erreicht ist

Die oben immer als Beispiel verwendete Ansteuerung des Computers woodstock sähe (mit GUI-Feedback) nun so aus:

define woodstock_STATE PRESENCE lan-ping woodstock 1 1
attr woodstock_STATE ping_count 1
attr woodstock_STATE devStateIcon present:on:off absent:off:on
attr woodstock_STATE eventMap /power on:on/power off:off/
attr woodstock powerCmd {powerCommand("", $NAME, $ARGUMENT, "", "wake", "sleepmac", 1)}
attr woodstock_STATE event-on-change-reading state


Ein anderes Beispiel: Ich habe einen Video-Projektor, der mit projector on|off gesteuert wird (da ich nur einen Video-Projektor habe, ist der Hostname fest codiert und muss nicht angegeben werden). Hier sähe die Definition wie folgt aus:

define Projektor PRESENCE shellscript '/usr/local/bin/projector numstate' 1 1
attr Projektor ping_count 1
attr Projektor devStateIcon present:on:off absent:off:on
attr Projektor eventMap /power on:on/power off:off/
attr Projektor powerCmd {powerCommand($NAME, "", $ARGUMENT, "projector", "", "", 1)}
attr Projektor event-on-change-reading state



Und hier schließlich der Code für die Perl-Subroutine, der nach 99_myUtils.pm kopiert werden muss. Ich hoffe, das hilft einigen, die ein ähnliches Einsatzszenario wie ich haben.

sub powerCommand($$$$$$$)
{
# arguments
my ($presenceName, $hostname, $action, $command, $onCommand, $offCommand, $guiFeedback) = @_;

# variables for building and executing the command
my $onCommandString;
my $offCommandString;
my $presenceState;
my $returnValue = "Internal error";

# variables for builing the success log message
my $successMessageOnCommandName;
my $successMessageOffCommandName;
my $successMessageDeviceName;
my $successMessage;

# sanity check
return "Configuration error: Either \$presenceName or \$hostname must be specified" if $presenceName eq "" && $hostname eq "";
return "Configuration error: Either \$command or \$onCommand and \$offCommand must be specified" if $command eq "" && ($onCommand eq "" || $offCommand eq "");

# initialize variables
$presenceName = $hostname if $presenceName eq "";
$presenceState = Value($presenceName);
$successMessageDeviceName = ($hostname eq "")? $presenceName : $hostname;

# build command strings and log message command names
# to be able to tell success from failure, stdout is ignored (= redirected to /dev/null), i.e. an empty return means success
# stderr is redirected to stdout so that error messages can be logged
if ($command eq "") # different commands for on and off
{
$successMessageOnCommandName = $onCommand;
$successMessageOffCommandName = $offCommand;
$onCommandString = "/usr/local/bin/$onCommand $hostname 2>&1 1>/dev/null";
$offCommandString = "/usr/local/bin/$offCommand $hostname 2>&1 1>/dev/null";
}
else # command with on|off argument for on and off
{
$successMessageOnCommandName = $successMessageOffCommandName = $command;
$onCommandString = "/usr/local/bin/$command $hostname on 2>&1 1>/dev/null";
$offCommandString = "/usr/local/bin/$command $hostname off 2>&1 1>/dev/null";
}

# depending on $action argument, build log message success string and execute command
if ("$action" eq "on" && $presenceState eq "absent")
{
$successMessage = "$successMessageOnCommandName: Waking \"$successMessageDeviceName\"";
fhem("setreading $presenceName state on") if $guiFeedback;
$returnValue = `$onCommandString`;
}
elsif ("$action" eq "off" && $presenceState eq "present")
{
$successMessage = "$successMessageOffCommandName: Putting \"$successMessageDeviceName\" to sleep";
fhem("setreading $presenceName state off") if $guiFeedback;
$returnValue = `$offCommandString`;
}
else {return 0;} # do nothing

# in case of success (= empty result string), log success message
Log 3, "PRESENCE ($presenceName) - $successMessage" if $returnValue eq "";

# return 0 (= success) or error message
return $returnValue;
}