Diese begonnene Einführung zielt auf Anwender, die zumindest kleine Vorkenntnisse im programmieren haben, denen aber die spezifischen Eigenarten von Shelly Scripts noch fehlen. Als (optionale) Vorkenntnisse sind solche in C nahen Programmiersprachen wie C++, Java oder insbesondere JavaScript wünschenswert. Ich vermag nicht einzuschätzen, wie gut das Skripten ganz ohne kleine Vorkenntnisse dieser Art gelingen kann. Ich beginne also ...

Im folgenden ist mit Skript oder Script immer ausschließlich ein Programmcode gemeint, welcher auf einem Shelly mindestens der zweiten Generation gespeichert wird und der vom Mikrocontroller des Shelly abgearbeitet werden kann.

Hier genutzte Links können auf Grund der Shelly Firmware Weiterentwicklung veralten und ggf. Ziele von Links nicht mehr erreichbar sein. Solche Fälle bitte ich zu entschuldigen.

Wo und wie ein Skript erstellt, gespeichert und gestartet (= zur Ausführung gebracht) werden kann, beschreibt die Shelly Dokumentation hinreichend - zum Script Tutorial.

Viele meiner Schritte folgen keiner spezifischen Anwendung für einen Nutzer, sie sind oft ausschließlich didaktisch zu verstehen. Aber alles lässt sich in Anwendungen zielgerichtet einsetzen.

Die Sprache von Shelly Scripts ist eine Untermenge von JavaScript. Es gelten somit die Sprachregeln von JavaScript. Die Shelly Dokumentation sagt dazu "Shelly Scripts run on a modified version of Espruino." - Quelle: Shelly Script Language Features. JavaSript ist eine Interpretersprache. Das bedeutet, dass mit dem Ablauf eines Skripts ein Programm, der Interpreter, den Skriptinhalt - lesbarer Text - in Anweisungen und Daten übersetzt, welche vom Mikrocontroller ausgeführt bzw. verarbeitet werden. Deshalb erscheinen Fehlermeldungen mit Skriptstop erst nach dem Starten eines Skripts, also zu dessen Laufzeit. 

  1. Erste Erfahrungen mit einem einfachen Skript - simple Ausgaben
    1. Ausgabe eines konstanten Textes
      Gib in einem neu angelegten Skript (Name: Einführung_1 oder nach Belieben) die folgende Anweisung ein!

      print("Dies ist ein konstanter Text."); // Kommentar. Das Auszugebende muss in runde Klammern gesetzt werden.

      Speichere das Skript und starte es anschließend! Falls du in der Konsole, der Bereich unterhalb des Editierfensters, keine Ausgabe siehst, muss das Protokollieren in der Web UI noch aktiviert werden. Manchmal ist das erneute Laden der Webseite erforderlich.
      Der Doppelslash (//) beginnt einen linksseitig terminierten Kommentar, dessen Ende das Zeilenende ist. Kommentare werden vom Interpreter übergangen, also nicht verarbeitet. Sie unterstützen das verständliche Lesen eines Skriptes.
      print() kann auch mehrere Dinge hintereinander ausgeben.
      Beispiel: print("aha", "soso", "na klar");

    2. Sobald das Skript gestartet wurde, wird es vom Skript Interpreter abgearbeitet. Dieser führt bei laufendem Skript auch Anweisungen aus, die in der untersten Zeile des Editierbereichs (Editierzeile, am unteren Ende des Konsolenfensters) eingegeben werden. Solche Anweisungen sind dafür geeignet
      • die Programmiersprache kennenzulernen, indem du eine Anweisung schlicht testest,
      • den aktuellen Inhalt von Variablen (im Skript beheimatet) auszugeben,
      • nach logischen Fehlern im Ablauf zu suchen.

An dieser Stelle sollst du mit der print Anweisung und sog. Ausdrücken experimentieren. Gib unterschiedliche print Anweisungen in der unteren Editierzeile ein - nicht im Skript selbst! Setze zwischen die runden Klammern hinter print unterschiedliche Ausdrücke wie 5+9, 3*7-6, "Die Summe aus zwei und acht ist"+(2+8) !
Sollte eine Ausgabe nicht deiner Erwartung entsprechen, nimm dies hin und versuche zu verstehen, wie diese Ausgabe zustande gekommen sein kann! Diese Skriptsprache basiert auf JavaScript, die nicht typenstreng ist. Die Folge davon ist, dass der Interpreter den Datentyp des Resultats aus dem Kontext (hier den Operanden) konstruiert. "3"+4 ergibt "34", weil der erste Operand "3" eine Zeichenkette (=Text, auch String genannt) ist, mit welcher nicht gerechnet werden kann. Stattdessen wird der zweite Operand, der Zahlenwert 4, in eine Zeichenkette transformiert und per Operator + an den ersten Operanden angefügt. Der Operator + hat also unterschiedliche, Kontext abhängige Bedeutungen. + mit mindestens einer Zeichenkette bewirkt die Verkettung der beteiligten Operanden, im Beispiel "3" verkettet mit "4" ergibt "34" (als Zeichenkette).
Im Ausdruck 3+4 hingegen ist + der Additionsoperator, weil beide Operanden (3, 4) vom Datentyp Zahl sind. Das Resultat ist demzufolge 7.

  1. Woraus besteht ein Skript? - oder - Wie ist ein Skript strukturiert?
    Ein Skript besteht schlicht aus Zeilen. Da ein Skript eine Textdatei ist, die von einem JavaScript Interpreter abgearbeitet wird, beinhalten alle Zeilen Text - nebst nicht druckbaren Sonderzeichen wie Zeilenvorschub, Tabulator, ...
    Eine Zeile beinhaltet einen Kommentar, eine (ausführbare) Anweisung, eine Definition oder Deklaration. Hier werden Definition und Deklaration synonym verwendet. Ich bevorzuge die Bezeichnung Definition. Einen Kommentar und eine Anweisung (print(...)) habe ich oben bereits verwendet. Eine Definition legt etwas fest, hier zumeist ein Unterprogramm, was hier Funktion genannt wird. Eine (nicht leere) Funktion beinhaltet Anweisungen, die erst mit einem Aufruf dieser Funktion abgearbeitet (=ausgeführt) werden. Variablen werden per Deklaration bzw. Definition angelegt.
    Falls du entsprechende Vorkenntnisse hast, wirst du evtl. fragen, an welcher Stelle die Abarbeitung des Skripts beginnt bzw. wo hier eine main-Funktion (Hauptfunktion, Hauptprogramm) zu platzieren ist.
    Antwort: Es gibt hier kein Hauptprogramm. - oder - Das Skript selbst ist das Hauptprogramm.
    (Fast) ausschließlich Anweisungen (engl. instructions) werden vom Interpreter ausgeführt. Kommentare übergeht er. Definitionen nimmt er zur Kenntnis. Er greift darauf zu, wenn dies eine Anweisung erfordert. Ein Anwendungs-Skript beinhaltet praktisch immer Definitionen und wenigstens eine Anweisung im Anschluss an solche Definitionen. Ohne zumindest eine Anweisung außerhalb aller Definitionen bewirkt ein Skript i.d.R. nichts, ist also i.d.R. nicht zielführend. Eine Anweisung kann auch in einer Definition als sog. Ausdruck enthalten sein.

Bsp.: value = 3 + 4; // 3 + 4 ist ein Ausdruck, in welchem die Anweisung zur Summenbildung steckt.

Nur der Vollständigkeit wegen: Einzige Ausnahme ist ein Skript, das wenigstens eine Funktion enthält, die von einer Quelle außerhalb des Skripts aufgerufen wird. Diese externe Quelle kann ein anderes Skript, ein Zeitplan oder ein anderes Gerät sein. Dies konkret zu verstehen erfordert aber weitergehende Kenntnisse und soll hier nicht vertieft werden.

  1. Werte im datentechnischen Sinn
    sind Inhalte, die etwas repräsentieren bzw. etwas aussagen. Der Wert 15.8 kann der Wert (ohne Einheit) einer Temperatur, einer Leistung, von Energie ... sein. Auch Texte sind Werte, aber keine Zahlen- sondern Zechenkettenwerte. Sie stellen Informationen in Fehlermeldungen, in Anweisungen oder in Nachrichten dar.
    Die Anweisung print(3+4); beinhaltet die Anweisung, 3+4 zu "bewerten", also den Resultatswert zu ermitteln, welcher schließlich als sog. Parameter an die aufgerufene print-Funktion zwecks Ausgabe übergeben wird.
    In einer Shelly-Anwendung ergeben sich Werte aus Messungen (per Messwandler), Uhrzeiten (mit Datum) und Zuständen. Solche Werte werden typischerweise in Nachrichten verpackt. Wenn bspw. ein sog. Ereignis gesendet wird, dann beinhaltet das Ereignis eine Nachricht, die strukturiert Werte enthält. Die Nachricht kann per Skriptfunktion in anwendungsgerechter Art ausgewertet werden.
    Wenn bspw. ein Ereignis eine Nachricht beinhaltet, die besagt, dass der per Ausgang eingeschaltete Verbraucher eine Leistung von über 1000 W (ein Wert) "zieht", dann kann per Skript dieser Verbraucher als Schutzmaßnahme ausgeschaltet werden. Werte können Zahlen, Zeichenketten (=Strings), null (für nichts, also quasi ein leerer bzw. ungültiger Wert), Referenzen (=Verweise auf eine Variable oder eine Funktion) oder eine Zusammenstellung aus allen solchen Werten sein. Letzteres wird in JavaScript als Objekt bezeichnet. Objekte können aber auch Funktionen beinhalten, was ich hier nicht erörtern möchte.

  2. Variable für sich ändernde Werte, wie Messwerte, Zeitwerte, Nachrichteninhalte
    Variable sind Behälter für Werte. Sie werden mit einem Namen versehen und mit ihrer ersten Verwendung per Schlüsselwort "let" (oder "var") deklariert und damit angelegt. Mit dem Anlegen einer Variablen wird ein Stück Arbeitsspeicher (RAM) dafür reserviert. Mit der Verwendung einer Variablen lässt sich das erste sehr kurze Skript ein wenig umgestalten sowie in der Eingabezeile (ganz unten) der Inhalt der Variable, also deren Wert, verändern.
    let value = "Dies ist ein Text als Inhalt der Variablen value."; // value ist der Name der hier angelegten Variablen. Dieser wird die Zeichenkette "Dies ..." als Wert zugeordnet.
    print(value); // Ausgabe des Wertes von value
    Jedesmal, wenn du in der Eingabezeile print(value); eingibst, wird der Wert der Variablen value ausgegeben. Dort kannst du auch den Wert von value verändern (ohne das Schlüsselwort let) und danach erneut ausgeben lassen.
    value = "Ein neuer Inhalt."; print(value);
    value = 12*4-6; print(value);
    Der Werttyp einer Variablen darf sich jederzeit ändern, vorher war in value eine Zeichenkette, nun liegt darin eine Zahl.

  3. Eine Funktion definieren und (später) aufrufen
    Eine Funktion ist zunächst ein benanntes (=per Name identifizierbares) Unterprogramm, welches von verschiedenen Stellen im Skript aufgerufen werden kann. Es gibt viele bereits definierte Funktionen, die einfach per Aufruf genutzt werden können. Zusätzlich können in einem Skript weitere Funktionen definiert und genutzt werden. Die Definition einer Funktion besteht aus ihrem Kopf und ihrem Rumpf. Der Kopf besteht aus dem einleitenden Schlüsselwort function, einem Namen für die Funktion, ein Paar runder Klammern und bei Bedarf einer Parameterliste. Der auf den Kopf folgende Rumpf besteht aus einem Paar geschweifter Klammern (ähnlich einem Schnurrbart, engl. moustache), innerhalb derer alle Anweisungen stehen, die beim Aufruf der Funktion abgearbeitet werden sollen. Die Funktionsdefinition wird nicht abgearbeitet. Sie legt fest, was die Funktion im Falle ihres Aufrufs tun soll. Wenn von der Funktion xyz die Rede ist, ist deren Definition gemeint.
    Beispiel einer Funktionsdefinition:
    function summe(a, b) { // Funktionsname ist summe, a und b sind zwei (Formal-)Parameter.
      let sum = a + b; // Berechnen der Summe aus dem Wert von a und dem Wert von b.
      return sum; // Kehre zur Aufrufstelle zurück und liefere das Resultat!
    }
    Aufträge:
    Ändere das bisherige Skript so ab, dass dieses ausschließlich die Definition dieser Funktion summe enthält!
    Dann starte das Skript! Wenn es eine Fehlermeldung gibt, liegt ein Syntaxfehler vor - vielleicht ist etwas falsch geschrieben, ein Zeichen kann fehlen, ... Dann korrigiere das Skript und versuche es erneut!
    Schließlich teste die Funktion summe per Eingabezeile, bspw. so:
    print(summe(3,6));
    Hiermit wird die Funktion summe aufgerufen und an diese die (Aktual-)Parameter 3 (für a) und 6 (für b) übergeben. summe liefert ihr Resultat, welches dann per print() ausgegeben wird.
    Experimentiere auch mit Ausdrücken! Bspw. so:
    print(12 + summe(7, 8));
    Welche Ausgabe erwartest du?

    Wie du vermutlich bereits erkannt hast, kann eine Funktion per Angabe ihres Namens, dahinter einem Paar runder Klammern und ggf. darin eingeschlossener Liste aus (Aktual-)Parametern aufgerufen werden. Die runden Klammern sind darin der Aufrufoperator. Mit dem Aufruf wird die Funktion abgearbeitet, sie erfüllt quasi einen per Parameter spezifizierten Auftrag. Hinter einem return darf auch nichts folgen. Dann arbeitet die Funktion etwas ab, ohne ein Resultat zu liefern. Allerdings ist das Liefern eines Resultats in den allermeisten Fällen nützlich und sollte deshalb vorgesehen werden. Das Resultat kann ein Wert sein, der sich aus der Verarbeitung der (Aktual-)Parameterwerte ergibt. Es kann aber auch ein Fehlercode oder eine Fehlernachricht sein. Ein Fehlercode 0 bedeutet nach heimlicher Übereinkunft i.d.R. "kein Fehler festgestellt", also Ok.
    An der Stelle eines Funktionsaufrufs kann das gelieferte Funktionsresultat verwendet werden oder auch nicht, d.h. verworfen werden.
    Beispiele:
    let x = 3 + summe(5, 4); // =3+9=12 - Das Resultat 12 wird der neu angelegten Variablen x zugewiesen.
    print(summe(7, 9) - 10); // 7+9-10=6 - Der Wert 6 wird an print als Parameter zwecks Ausgabe übergeben.
    summe(4, 7); // Mit dem Resultat 11 wird nichts getan, diese Anweisung ist wirkungslos.


    Funktionen sind nützliche Bestandteile eines Skripts. Mit ihnen kannst du dein Skript übersichtlich gestalten und Teile auf Zuständigkeiten verteilen. Eine Funktion ist immer auch zuständig für etwas. Dies entspricht der menschlichen Arbeitsteilung.

    Hinweis:
    Obige Funktionsdefinition summe kann auch kürzer geschrieben werden.
    function summe(a, b) {return a + b;}
    Hiermit wird bei Aufruf die Summe aus den Parameterwerten berechnet und danach das Resultat an die Aufrufstelle geliefert - ohne eine zusätzliche Variable. Ein Ausdruck hinter return darf noch erheblich komplexer sein.

  4. Eine Funktion als Parameter
    Hierbei wird mit dem Aufruf einer Funktion eine andere Funktion als (Aktual-)Parameter übergeben. Ein solcher Funktionsparameter besteht ausschließlich aus dem Namen einer Funktionsdefinition.
    Ein einfaches, rein didaktisches Beispiel:
    function f1(Parameter) {print("f1:", Parameter);}
    function f2(x) {print("f2:", x);}
    function summe(a, b, f) {f(a+b);}

    Was erwartest du mit den folgenden Aufrufen?
    summe(3, 4, f1); // Aufruf 1
    summe(5, 7, f2); // Aufruf 2

  5. Unbenannte Funktionen
    sind Funktionen ohne Namen. Sie werden auch als anonyme Funktionen bezeichnet. Solche Funktionen können somit nicht wie oben per Angabe eines Namens aufgerufen werden. Sie sind ausschließlich als Parameter beim Aufruf einer anderen Funktion geeignet. Hier verwischt sich der Unterschied zwischen Definition und Aufruf. Anonyme Funktionen sind nicht erforderlich, mit ihrer Verwendung kann ein Skript etwas kürzer gestaltet werden. Anfängern empfehle ich, solche anonymen Funktionen zu vermeiden. Der Vollständigkeit wegen will ich hier zum obigen Beispiel die Verwendung einer anonymen Funktion zeigen.
    function summe(a, b, f) {f(a+b);} // Definition der Funktion summe mit 3 formalen Parametern
    Dass als dritter Parameter (f) eine Funktion (genau genommen deren Referenz) erwartet wird, erkennt der JavaScript Interpreter am Aufrufoperator "(...)" in der Anweisung f(a+b).
    Verwendung einer anonymen Funktion beim Aufruf von summe:
    summe(3, 4, function (result) {print("anonyme Funktion:", result);} );
    Du erkennst vermutlich, dass die Syntax bei Verwendung einer anonymen Funktion gewöhnungsbedürftig ist. 😉
    Trotzdem der Versuch einer Erklärung.
    Mit dem Aufruf von summe werden die beiden Zahlparameter 3 und 4 übergeben. Als dritter Parameter wird die darauf folgende unbenannte Funktion übergeben, die selbst einen Parameter (result) erwartet. Letztere gibt den Text "anonyme Funktion:" gefolgt vom Wert des Parameters result aus. Was läuft hierbei ab?
    Die Funktion summe berechnet die Summe der Parameterwerte von a und b, hier also 3+4. Dann verwendet sie den Parameter f zu einem Funktionsaufruf mit dem Resultat, hier 7, als (Aktual-)Parameter. Der Parameter f wird vom Interpreter in diesem Kontext als Referenz, also als Verweis, auf die hier anonyme Funktion genutzt, welche beim Aufruf von summe als dritter Parameter definiert ist. JavaScript ist tatsächlich nicht konsequent in ihrer Notation, weil so manches nur im Kontext verständlich ist. Wenn du nun Knoten in deinem Hirn verspüren solltest, mache dir nichts daraus! Das erscheint wenigstens mir für einen Anfänger normal. 

Die oben aufgezeigten Dinge sind alle für ein Verstehen von Skriptbeispielen erforderlich, aber noch nicht hinreichend. Aus diesem Grund habe ich die obigen Punkte bereits in diesem ersten Teil versucht zu erklären. Zumindest für ein späteres Nachschlagen sollen diese Punkte geeignet sein.

Ich kann viele Experimente zwecks Erkenntnisgewinnung sehr empfehlen. Dabei ist es zielführend, Fehler zu machen und gelegentlich auch mal etwas so passend zusammenzustellen, dass das erwartete Ergebnis herauskommt.

In diesem Teil habe ich wichtige Elemente eines Skripts beschrieben, die zum Verständnis eines bereits vorhandenen und anwendbaren Skripts erforderlich sind. Du hast vermutlich ein Projekt vor Augen, welches du möglichst schnell implementieren willst. Ich rate dir aber, vor diesem Projektvorhaben eine Phase des Experimentierens und Verstehens einzubauen. Solche Phasen solltest du immer dann nutzen, wenn du etwas neues angehen willst, zu dem du noch keine oder wenig Erfahrung besitzt.

Der zweite Teil dieser Einführung führt zu Skripten, die bereits einfache Anwendungen bereitstellen. Falls du daran interessiert bist, bleibe am Ball!

"Unmögliches wird sofort erledigt, Wunder dauern etwas länger." 😉

zu Teil 2

2024-03-09