In Teil 1 sind wichtige Dinge unter Verwendung didaktischer Beispiele beschrieben. Diese Dinge sind zur Nutzung eines anwendungsbezogenen Skripts erforderlich. In diesem Teil 2 sollen erste Skripte zusammengestellt werden, die einfache Anwendungen ermöglichen. Wenn im folgenden von Skript die Rede ist, ist damit immer ein Skript für Anwendungen gemeint.
Skripte sind Ereignis gesteuert. Ein solches Ereignis kann ein Zeitereignis, ein Statusereignis oder ein Nachrichtenereignis sein. Zeit- und Statusereignisse sind lokal ausgelöst, also auf demselben Shelly, auf welchem das Skript läuft. Ein Nachrichtenereignis kann lokal ausgelöst werden, dürfte aber zumeist von einer externen Quelle stammen. Eine solche externe Quelle kann ein anderer Shelly, ein übergeordnetes System (ioBroker, Home Assistant, openHAB, ...), ein Web Browser auf einem Arbeitscomputer oder (vermutlich) die Shelly Cloud sein.
In diesem Teil 2 gehe ich nicht auf Nachrichtenereignisse ein.
Als Gerät zum experimentieren und testen der Skripte kann ein Shelly Plus 1 (PM), ein Plus 2PM, ein Plus Plug S, ein Plus Uni oder ein Gerät der dritten Generation mit zumindest einem Ausgang genutzt werden. Diese Liste ist unvollständig. Das Gerät darf an einem Eingang mit einem Schalter oder Taster elektrisch verbunden sein und darf einen Verbraucher, bspw. eine Lampe, schalten. Du kannst den Shelly aber auch ganz ohne eine solche Beschaltung einsetzen.
Wichtig!
Das Gerät sollte keine zusätzlichen Anschlüsse nutzen und auf ihm sollten kein Zeitplan und keine Action aktiv sein. Solche Dinge können zur Verwirrung beitragen und sollten deshalb unterbleiben.
Wenn du keine Beschaltung des Eingangs verwendest, öffne im Web Browser einen weiteren Tab oder starte einen anderen Browser für die Web UI des Shelly! Damit wirst du den Ausgang per Klick schalten können. Die Web UI kannst du selbstverständlich auch bei der Beschaltung des Eingangs verwenden.
- Erfassen eines Ereignisses - hier: Das Schalten eines Ausgangs feststellen
Das Erfassen eines Ereignisses ist die zumeist erforderliche Grundlage für weiterführende Anwendungen. Hier soll zunächst nur festgestellt werden, dass der Ausgang 0 des Shelly geschaltet wurde - unabhängig von der auslösenden Quelle dieses Schaltens. Hierfür ist die Definition einer Funktion zur Ereignisverarbeitung nötig, auch Ereignisbehandlung genannt (engl. event handling). Diese Funktion soll hier den Namen "checkEvent" erhalten. Sie muss für diesen Zweck Informationen zum eingetroffenen Ereignis per Parameter importieren. Diesen Parameter nenne ich "event".// Definition der Funktion zur Verarbeitung von Ereignissen
function checkEvent(event) {
print(event);
}
Erstelle ein Skript mit dieser Funktionsdefinition oder ändere ein vorhandenes Skript entsprechend ab!
Diese Funktion gibt schlicht etwas aus, was ihr mit einem Aufruf als Parameter übergeben wird.
Teste diese Funktion, indem du in der Eingabezeile (ganz unten) checkEvent aufrufst und dabei irgendetwas übergibst, bspw so:checkEvent("irgendetwas");
Du wirst sehen, dass diese Funktion sehr schlicht ist und so gar nichts außergewöhnliches tut. Auch kann sie keine Ereignisinformation per Parameter event importieren. Sie wird erst dann zu einer Funktion, die auf Ereignisse reagiert, wenn sie in der Firmware als sog. EventHandler registriert wird.
Hierfür stellt die Firmware die Funktion Shelly.addEventHandler(...) zur Verfügung. Beachte den erforderlichen Punkt zwischen Shelly und addEventHandler. Dieser Funktionsname ist aus zwei Teilen zusammengesetzt - Shelly und addEventHandler. Der Punkt verbindet den übergeordneten Teil Shelly mit dem untergeordneten addEventHandler. Im so zusammengesetzten Namen darf, wie in anderen solchen Namen auch, kein Leerzeichen enthalten sein.
Für mit Vorkenntnissen Behaftete: Shelly ist ein Objekt, welches die Methode addEventHandler beinhaltet.
Um unsere Funktion checkEvent als EventHandler zu registrieren, ist eine Anweisung erforderlich, die Shelly.addEventHandler(...) aufruft.Shelly.addEventHandler(checkEvent); // registriert checkEvent als EventHandler
Hier brauchst du das gelieferte Resultat nicht, da die Registrierung dieses EventHandler im Skriptablauf nicht mehr entfernt wird.
Ergänze das Skript am Ende mit dieser Anweisung! Nun wird es spannend ...
Starte das Skript, wenn es nicht bereits läuft!
Schalte den Ausgang irgendwie ein bzw. aus - per Schalter/Taster oder per Klick auf das Symbol in der Web UI!
Betrachte dann die Ausgabe im Protokollfenster! Bewahre dabei Gelassenheit! 😉
Wenn irgendwann zuviel im Protokollfenster steht, kannst du dessen Inhalt mit einem Klick auf CLEAR LOG (oben links) löschen. Falls auch Ereignisse auftreten sollten, die du nicht per schalten des Ausgangs ausgelöst hast, ignoriere diese!
In der Protokollausgabe wird so etwas wie folgt stehen.{"component": "switch:0","name": "switch","id": 0, "now": 1709454469.02190089225,"info": {"component": "switch:0","id": 0,"event": "toggle","state": true, "ts": 1709454469.01999998092 }}
Die Ausgabe der Ereignisinformationen (welche per Parameter event importiert werden) ist etwas komplex. Sie sind per spezieller Notation zusammengesetzt. Diese Notation wird JSON genannt (JavaScript Object Notation) und bewährt sich in vielen Anwendungen, auch in solchen, die nicht per JavaScript implementiert sind. Eine JSON Zeichenkette ist immer ein in Textform beschriebenes sog. Objekt, das hierarchisch gegliedert ist.
Was kannst du der obigen Protokollausgabe entnehmen?
Die gesamte Information steht in einem Paar geschweifter Klammern.
Sie beinhaltet eine Aufzählung (Liste) von Einträgen, an den Kommata erkennbar.
Jeder Eintrag besteht aus zwei Teilen, die per Doppelpunkt getrennt sind.
Beispiel eines Eintrags: "component": "switch:0"
Vor dem Doppelpunkt steht der sog. Schlüssel (key), hier "component". Der Schlüssel ist immer eine Zeichenkette. Hinter dem Doppelpunkt steht der Wert, hier "switch:0", welcher dem Schlüssel zugeordnet ist. Dies entspricht einer Variablen mit dem Variablennamen als Schlüssel und deren Inhalt/Wert der Variablen. Der Wert kann vieles sein - ein Wahrheitswert (true, false), eine Zahl, ein Text (Zeichenkette), eine Funktion (Referenz), ein Objekt (etwas komplexes). Später werde ich zeigen, wie du mit Hilfe eines solchen Schlüssels auf den Wert des Eintrags zugreifen kannst. Bereits an dieser Stelle sollte verständlich werden, dass du per "component"-Wert in der EventHandler-Funktion das selektieren kannst, was in deiner Anwendung zum Zuge kommen soll.
Besonders interessant ist der Eintrag mit dem Schlüssel "info", weil dieser weitere Einträge wie "event" und "state" enthält. An diesen ist erkennbar, was am Ausgang "switch:0" geschehen ist und welchen Zustand er nun hat. "event":"toggle" bedeutet, dass der Ausgang umgeschaltet wurde, "state":true, dass er nun auf "ein" steht (also ein angeschlossener Verbraucher eingeschaltet ist). true ist ein Wahrheitswert, sein gegenteiliges Pendant ist false.
"info" ist ein Objekt im übergeordneten Objekt "event" (Parametername der Funktion checkEvent()). Dies magst du vergleichen mit einem Objekt Schublade in einem übergeordneten Objekt Schrank.
Es ist völlig normal, wenn Ereignisse von unterschiedlichen Komponenten (sog. Components) des Shelly (genauer: der Shelly Firmware) eintreffen. Deshalb ist eine Selektion nach solchen Components zielführend, was im nächsten Punkt angegangen wird.
Da die Protokollausgabe viele Zeilen umfasst, sollst du den Aufruf einer Funktion kennenlernen, welcher eine platzsparendere Ausgabe ermöglicht und in anderen Kontexten erforderlich ist. Diese Funktion heißt JSON.stringify(). Sie liefert zu einem übergebenen Objekt (hier event) die JSON-Zeichenkette, in welcher keine Zeilenvorschübe enthalten sind. Ersetze hierfür in der Funktion checkEvent() die Ausgabeanweisungprint(event);
durch folgende.print(JSON.stringify(event));
Damit erscheint die Protokollausgabe in nur einer Zeile, die umbrochen wird, falls die Zeichenkette zu lang ist.
Ausgabebeispiel:
{"component":"switch:0","name":"switch","id":0,"now":1709460261.17189645767,"info":{"component":"switch:0","id":0,"event":"toggle","state":false,"ts":1709460261.16999983787}}
Hinweis:
In der Dokumentation zu Shelly.addEventHandler() ist ein weiterer Parameter "userdata" aufgeführt, der formal in vielen Beispielen überflüssigerweise zu finden ist. Für eine Einführung ist dieser Parameter schlicht Bullshit. Also lassen wir ihn weg. Wenn du irgendwann Bedarf an diesem Parameter haben solltest, wirst du vermutlich fähig sein, die Dokumentation verstehend zu lesen und insbesondere mit einem solchen Parameter zu experimentieren. Vielleicht werde ich in einem späteren Teil dieser Einführung noch darauf eingehen, wozu aber weitergehende Kenntnisse erforderlich sind. - Zugriff auf Teile der Ereignisinformation, Selektion von Ereignissen
Das bisherige Skript mit dem EventHandler checkEvent() wird weiterhin verwendet.
Im weiteren Verlauf werde ich statt Zeichenkette das dafür international gebräuchliche Wort String verwenden. Das ist zweckmäßig, weil es in JavaScript String-Objekte gibt. Für die Verarbeitung solcher String-Objekte stehen Objekt eigene Funktionen zur Verfügung. Funktionen, die Objekt eigen sind, werden Methoden genannt. Eine solche Methode wird per Objektname, gefolgt von einem Punkt und dem Methodennamen aufgerufen. Siehe hierzu auch JavaScript String Reference.
Zunächst eine weniger geeignete Variante zur Selektion von Ereignissen.
Prinzipiell kann eine Zeichenkette per String-Methode indexOf() nach einem Teilstring abgesucht werden. Du könntest somit in der Event-Information bspw. nach dem Teilstring "switch:0" suchen lassen.let eventString = JSON.stringify(event); // zu event einen String erzeugen
let switchPos = eventString.indexOf("switch:0"); // nach dem Beginn von "switch:0" in eventString suchen
if(switchPos >= 0) { // wenn "switch:0" in eventString vorkommt
// Ereignis verarbeiten
}
Diese Variante ist relativ aufwändig und insbesondere fehlerträchtig. Fehlerträchtig, weil eventString irgendwo einen solchen Teilstring enthalten kann, auch als Schlüssel - und das ist hier nicht gemeint. Ein Zugriff auf einen Wert sollte grundsätzlich per Schlüssel erfolgen. Und das gelingt wie folgt.
Der in checkEvent() unter dem Namen event importierte Parameter ist ein Objekt. Oben sprach ich von "Einträgen" in der Ereignis-Information. Ich verwende im Rahmen eines Objektes gerne das Wort "Komponente", welches du bitte nicht mit den Shelly Components verwechseln solltest, auch wenn beide ähnlich sind. Das Wort Komponente werde ich ausschließlich für einen Teil (entsprechend einem Eintrag) eines Objektes verwenden. Meine ich eine Shelly Component, werde ich dafür Component notieren.
Der Zugriff auf eine Komponente (als Teil eines Objektes) gelingt per Schlüssel seines Eintrags.
Hier noch einmal die oben dargestellte kompakte Ausgabe von event:
{"component":"switch:0","name":"switch","id":0,"now":1709460261.17189645767,"info":{"component":"switch:0","id":0,"event":"toggle","state":false,"ts":1709460261.16999983787}}
event enthält den Schlüssel "component" an zwei Stellen, wie den Schlüssel "id" auch. Zunächst will ich den ersten Eintrag mit dem Schlüssel "component" verwenden.
Das Objekt event besteht aus 5 Komponenten, deren Namen bzw. Schlüssel "component", "name", "id", "now" und "info" lauten. "info" ist selbst ein (untergeordnetes) Objekt aus 5 Komponenten - dazu später mehr. Der Zugriff auf den Wert einer Komponente gelingt mit Objektname.Schlüssel oder anders interpretiert Objektname.Komponentenname. Um im Objekt event auf den Wert zum Schlüssel "component" zuzugreifen, notieren wir den Ausdruck event.component. Der Komponentenname ist also gleich dem Schlüssel, aber ohne die Anführungszeichen.
Frage: Wie muss der Ausdruck lauten, mit dem du auf den Wert zum Schlüssel "name" im Objekt event zugreifen kannst?
Die Shelly Components finden sich in den Ereignis-Informationen wieder. Wenn der Zustand eines Ausgangs (Relais, Optokoppler) verändert wurde, führt dies zu einem Ereignis. Dieses Ereignis beinhaltet dann zum Schlüssel "component" als Wert die Component Switch (mit Id), bspw. "switch:0".
Nun zur Selektion:
Um in checkEvent ausschließlich Ereignisse zu verarbeiten, die der Shelly Component Switch mit Id 0 zugeordnet sind, lassen wir event.component auf den Wert "switch:0" prüfen.if(event.component==="switch:0") {
// Ereignis verarbeiten
}
Dies soll nun in den EventHandler checkEvent() eingebaut werden.function checkEvent(event) {
// print(event);
if(event.component==="switch:0") { // === vergleicht auf Übereinstimmung in Wert und Typ
print("Ausgang 0 wurde geändert.");
}}
Die erste print-Anweisung habe ich auskommentiert, also in einen Kommentar umgewandelt, damit sie nicht ausgeführt wird. Wenn du diese Ausgabe noch brauchen solltest, entferne einfach den Doppelslash!
Statt der schlichten Ausgabe eines Textes kann selbstverständlich etwas effektiveres eingebaut werden, wie das Senden einer Nachricht per MQTT oder/und HTTP. Du wirst vielleicht einwenden, dass zumindest eine HTTP-Nachricht auch leicht per Action Konfiguration erreicht werden kann und sich die Mühe einer Skripterstellung nicht lohnte. Das stimmt zunächst und nur dafür konfiguriere auch ich lieber eine Action. Du wirst aber hoffentlich noch erkennen, dass mit einem Skript noch ganz andere Dinge erreichbar sind, die mit Actions nicht möglich sind. Also bleibe dran! 🙃
Anregung
Wenn es dich packen sollte und du mittlerweile genügend grundlegende Sicherheit im skripten hast, versuche herauszufinden, wie du per Skript eine HTTP-Nachricht senden lassen kannst! Dann baue das Senden einer geeigneten Nachricht an ein anderes Gerät ein und teste dies! Übrigens, auch eine MQTT-Nachricht lässt sich leicht per Skriptanweisung veröffentlichen. - Zugriff auf eine tiefer liegende Objekt-Komponente
Hier noch einmal die oben dargestellte kompakte Ausgabe von event:
{"component":"switch:0","name":"switch","id":0,"now":1709460261.17189645767,"info":{"component":"switch:0","id":0,"event":"toggle","state":false,"ts":1709460261.16999983787}}
Angenommen, du willst feststellen lassen, was mit dem Ausgang geschah und welchen Schaltzustand er nun hat. Wo kannst du diese Informationen finden? Suche vor dem Weiterlesen bitte zuerst selbst!
...
Diese Informationen stecken in der Komponente info. Dort sind sie als Komponenten event und state enthalten. Der Zugriff auf diese Komponenten ist leicht und folgt einer eindeutigen Regel: Der Zugriff auf eine Komponente gelingt mit dem Punkt, gefolgt vom Komponentennamen.
Wie lautet der Ausdruck für den Zugriff auf die Komponente state im Objekt info, welches eine Komponente des Objektes event ist? Ich lasse dir Zeit zum verstehen, denken und experimentieren. 😇
...
Der Zugriffsausdruck gestaltet sich vom Gesamten zum Detail. Das Gesamte ist das Objekt event, das Detail ist die Komponente state im Objekt info. info liegt also auf dem Weg von event zu state zwischen diesen beiden. Der Zugriffsausdruck lautet somitevent.info.state
. Dieser Ausdruck kann dazu genutzt werden, auf Grund des Ereignisses der Component Ausgang "switch:0" den aktuellen Zustand festzustellen. Um diesen Zustand auszugeben kannst du eine zweiseitig bedingte Anweisung per if ... else ... verwenden.// event.info.state hat einen Wahrheitswert (true oder false)
if(event.info.state) print("Ausgang 0 ist eingeschaltet.");
else print("Ausgang 0 ist ausgeschaltet.");
Dazu zwei Hinweise:
Die geschweiften Klammen hinter if(...) wie auch hinter else sind ausschließlich für Anweisungsblöcke (mit mehreren Anweisungen) erforderlich. Folgt nur eine Anweisung, können sie weggelassen werden.
In den runden Klammern hinter if muss immer ein boolescher Ausdruck stehen. Das ist ein Ausdruck, dessen Resultat (=Wert) true oder false ist. Ich finde immer wieder in Quellcodes (=Programmiertexte) den Vergleich eines booleschen Ausdrucks mit true, was hier alsif(event.info.state===true) ...
notiert wäre. Das ist eine Notation, ohne darüber nachgedacht zu haben und wenig sinnreich, da event.info.state bereits einen Wahrheitswert besitzt.
Ersetze im EventHandler checkEvent die bisherige Ausgabe per print("Ausgang 0 wurde geändert.") durch obige zweiseitig bedingte Anweisung (if ... else ...)!
Und nun noch eine kleine Optimierung - ich kann mich damit nicht zurückhalten. 😋
Der einzige Unterschied in beiden Ausgaben ist der Textteil "ein" bzw. "aus". Hier bietet sich der sog. trinäre Operator an. Er heißt trinär, weil er drei Operanden verarbeitet. Er besitzt die Form boolescher Ausdruck ? Ausdruck1 für true : Ausdruck2 für false .
Das Resultat dieses trinären Operators ist der Wert von Ausdruck1, wenn der boolesche Ausdruck vor ? true ergibt, andernfalls ist das Resultat der Wert von Ausdruck2.
Damit kann die Ausgabe ohne if ... else ... so erzeugt werden:print("Ausgang 0 ist " + (event.info.state ? "ein" : "aus") + "geschaltet.");
Schließlich bleibt noch die Frage nach dem Zugriff auf die Komponente event in info. Auch wenn dies etwas merkwürdig aussieht, der Ausdruck lautet event.info.event. Das vordere event ist der Name des Parameters im EventHandler, das hintere event ist der Komponentenname in info. Wenn du den Parameter in der Definition von checkEvent() bspw. para nennst, gelingt der Zugriff per para.info.event. Die Komponente event in info hingegen kannst du nicht umbenennen, weil deren Name (=key) von der Firmware vorgegeben ist. - Zielgerichtet nach Ereignissen suchen und verarbeiten
Was immer du mit einem Shelly Plus oder Pro tun willst, Ereignisse und deren Behandlungsfunktion werden sehr oft von Bedeutung sein. Sei es, dass du auf Grund eines Ereignisses etwas auslösen lassen willst. Sei es, dass du dann eine Nachricht, bspw. zwecks Rückmeldung, senden lassen willst. Es gibt hierfür viele Anwendungsmöglichkeiten.
Was du auch auslösen lassen willst, wesentlich ist das Ereignis selbst - oder genauer: die Informationen zum Ereignis.
Es gibt unterschiedliche Quellen von Ereignissen. Auch die Informationen, welche per Parameter des EventHandlers eintreffen, sind unterschiedlich strukturiert. Wie du dir solche Informationen ansehen kannst, weißt du bereits. Das gelingt perprint(event);
oderprint(JSON.stringify(event));
. event ist der Name des Parameters in der Definition der EventHandler-Funktion.
Um nach passenden Ereignissen zu suchen, ist deren Auslösung erforderlich. Wenn du bspw. den EventHandler auf eine Änderung eines Eingangs wie "input:0" als Component reagieren lassen willst, brauchst du erst einmal die dazu passenden Ereignis-Informationen. Vielleicht weißt du noch nicht, wie die Komponente heißt. Du kannst jedenfalls etwas auslösen, was zum gewünschten Ereignis führt, bspw. einen Schalter oder Taster an einem Shelly-Eingang betätigen.
Wenn du solches tust und die Ausgabe des EventHandlers analysierst, kannst du die Informationen ermitteln, die du zur Selektion brauchst. Der Rest ist das Schreiben geeigneter Anweisungen.
Aufgabe
Beschalte den Eingang 0 des Shelly mit einem Taster oder Schalter!
Stelle in der Konfiguration des Shelly den Input 0 auf detached! So kann eine Betätigung des Tasters/Schalters den Ausgang nicht unmittelbar schalten. Teste dies, um die Nichtwirkung des Betätigens zu prüfen!
Du kannst selbstverständlich eine Action erstellen, die den Ausgang schaltet, wenn der Taster/Schalter betätigt wird. Hier ist aber das Ziel, dies per Skript zu erreichen.
Stelle zunächst fest, welche Ereignis-Informationen eintreffen, wenn du den Taster/Schalter betätigst!
Selektiere im EventHandler per bedingter Anweisung (if ...) das passende Ereignis, bspw. per component oder name und id!
Lasse auf Grund der Ereignis-Informationen den Ausgang passend schalten!
Hierfür stehen sog. RPC (Remote Procedure Call) Methoden wie "Switch.Set" und "Switch.Toggle" zur Verfügung. Solche RPC Methoden lassen sich im Skript per Shelly.call(...) aufrufen. Um bspw. den Ausgang 0 auf "on" zu stellen, kannst du folgende Anweisung verwenden.Shelly.call("Switch.Set", {id:0, on:true});
Dies ist die einfachste Form ohne die callback Funktion und soll erst einmal genügen. Zu callback Funktionen kann ich in einem anderen Teil ggf. mehr erklären. Der zweite Parameter im Aufruf von Shelly.call() ist ein Objekt, welches aus den Komponenten id und on besteht. Wenn du ausschalten lassen willst, ersetze den on-Wert true durch false.
Starte das Skript und teste es! Nun sollte trotz der detached Einstellung der Ausgang 0 per Taster/Schalter am Eingang 0 schaltbar sein, solange das Skript läuft. Das Schalten des Ausgangs sollte nicht gelingen, wenn das Skript gestoppt ist.
Der AufrufShelly.call("Switch.Toggle", {id:0});
bewirkt das Umschalten des Ausgangs 0.
Der erste Parameter ist ein String, welcher den Methodennamen enthält. Der zweite Parameter darf statt Objekt ein String sein, welcher im JSON-Format ein Objekt repräsentiert. Ich finde es angemessener, dafür gleich das Objekt zu notieren, wie {id:0, on:true} oder {id:0}. Die Struktur eines solchen Objektes hängt von der Methode ab. Zu "Switch.Toggle" ist nur die id Komponente erforderlich, weil umschalten eindeutig ist. "Switch.Set" hingegen braucht zwei Komponenten, weil mit dem on-Wert der gewünschte Zustand anzugeben ist. Es gibt auch RPC-Methoden, die keinen Parameter erfordern. Dann sollte das Objekt leer sein, was per {} notiert wird, also das Paar geschweifter Klammern mit nichts dazwischen.
Du kannst solche RPC-Methoden per URL testen, bevor du sie in einem Skript per Shelly.call() nutzt. Dazu gibt es auf dieser Website den Artikel Methodenaufrufe in einem Skript.
Anmerkung
Der Vorteil der obigen Skript-Lösung liegt darin, dass fast beliebige, zusätzliche Bedingungen eingebaut werden können, die das Schalten erlauben. So kann bspw. eine von einem anderen Gerät eintreffende Nachricht das Schalten freigeben oder sperren. So etwas gelingt weder mit einer Action noch mit einer Szene in der Cloud. 😁
zu Teil 1
zu Teil 3
2024-03-06