logoFHEM CONTROL 2.0



Schnelleinstieg mit praktischen Beispielen

1 Definition der Grundbegriffe

2 Einfache Beispiele für den Einsatz von λscript in einer fhem-Installation

2.1 Synchronisation von Schaltzuständen

Die Eingabe von "define test lambda" in der Kommandozeile der fhem-Website erzeugt ein Device mit dem Namen test. Der Eintrag in Feld DEF enthält das Script. Die Ausführung des set-Befehls "set test start" führt das Script aus. Es schreibt "Hallo FHEM" in die aktuelle log-Datei.

Mit einem Klick auf DEF kann das Script editiert werden. Angenommen es gibt in der Installation ein fhem-Device WZ_Lampe lässt sich diese Lampe mit den folgenden Kommandos für 3 Sekunden einschalten:

set WZ_Lampe on;
wait 0,3;
set WZ_Lampe off;

Der dem Symbol wait folgende Wert ist vom Typ timespan und bezeichnet eine Zeitdauer von 3 Sekunden. Allgemein wird eine Zeichenfolge m/d/h:min,sec als eine Zeitspanne von m Monaten, d Tagen, h Stunden, min Minuten und sec Sekunden interpretiert. Der Doppelpunkt oder das Komma ist zwingend, damit λscript die Zeichenfolge als timespan interpretiert.

Soll dass Ein- und Ausschalten fünf mal wiederholt werden, wird das Script so erweitert:

repeat 5 {
   set WZ_Lampe on;
   wait 0,3;
   set WZ_Lampe off;
   wait 0,3;
}

Das Kommando zum wiederholten Ausführen von Anweisungem wird vom Schlüsselwort repeat eingeleitet. Danach folgen die Zahl der Wiederholungen (Eine Zahl oder eine Variable vom Typ number) und ein Block mit den Kommandos die wiederholt werden sollen. Wird die Zahl 5 nach dem Symbol repeat weggelassen, wird der nachfolgende Block unendlich oft ausgeführt.

Angenommen es gibt in der fhem-Installation noch ein Device Flur_Lampe. Ziel soll sein, ein Script zu schreiben, dass WZ_Lampe immer eingeschaltet wird, wenn Flur_Lampe eingeschaltet wird. Ein Script dafür wäre:

repeat {
   wait [Flur_Lampe];
   case ([Flur_Lampe] ~ /on/) {set WZ_Lampe on};
}

Hier passiert Folgendes: Zunächst wird auf eine Änderung des Readings state des Devices WZ_Lampe gewartet. Allgemein liefert der Ausdruck [deviceName readingName] den aktuellen Wert des Readings mit dem angegebenen Namen. Der Rückgabewert ist immer ein Object der Klasse string. Für das Reading state kann readingName weggelassen werden. Dem Symbol wait können beliebig viele Parameter folgen. Bei einem Parameter vom Typ timespan wird mit dem Script nach dem Ablauf dieser Zeitspanne fortgefahren. Ist der Parametern vom Typ moment wird das Script nach dem Erreichen dieses Zeitpunktes fortgesetzt. Ist ein wait-Parameter weder von Typ timespan noch von Typ moment so wird mit der Fortführung des Scripts gewartet bis sich der Wert des Parameters geändert hat. Folgen dem Symbol wait mehrere Parameter, wird das Warten beendet wenn das entsprechende Ereignis für einen der Parameter eintritt.

Nachdem sich also das Reading state von Flur_Lampe geändert hat, wird geprüft, ob der string "on" in state enthalten ist. Hier wäre auch der Vergleich [Flur_Lampe] = "on" möglich. Die erste Variante schließt jedoch den Fall, dass state den Wert "set_on" hat, ein. Ist der Vergleich erfolgreich, wird der nachfolgende Block {set WZ_Lampe on} ausgeführt. Danach wird die Kommandofolge mit dem wait-Befehl wiederholt. Es wird wieder auf einen Veränderung des Readings state von Flur_Lampe gewartet.

Folgt im case-Kommando ein zweiter Block, wird der ausgeführt, wenn die Bedingung nicht erfüllt ist. Das nachstehende Script bewirkt, das WZ_Lampe parl zu Flur_Lampe geschaltet wird.

repeat {
   wait [Flur_Lampe];
   case ([Flur_Lampe] ~ /on/)
      {set WZ_Lampe on}
      {set WZ_Lampe off}
}

Um sicher zu stellen, das WZ_Lampe nur eingeschaltet wird, wenn sie nicht schon an ist (und umgekehrt), werden noch zwei case-Kommandos eingefügt.

repeat {
   wait [Flur_Lampe];
   case ([Flur_Lampe] ~ /on/)
      {case ([WZ_Lampe] !~ /on/) {set WZ_Lampe on}}
      {case ([WZ_Lampe] !~ /off/) {set WZ_Lampe off}}
}

Um die erfolgreiche Erstellung dieses Scripts im fhem-Protokoll festzuhalten wird das Script um eine Zeile ergänzt:

repeat {
   wait [Flur_Lampe];
   case ([Flur_Lampe] ~ /on/)
      {case ([WZ_Lampe] !~ /on/) {set WZ_Lampe on}}
      {case ([WZ_Lampe] !~ /off/) {set WZ_Lampe off}}
};
log "Mein erstes lambda-script!"

Wichtig ist hier das Semikolon nach dem repeat-Block. Das repeat-Kommando und das log-Kommando sind zwei Kommandos die durch ein Semikolon getrennt werden müssen!

Nach dem Start des Scripts läuft zwar die Kopplung der Lampen wie gewünscht, aber der Eintrag im Protokoll findet nicht statt. Warum? Die Zeile log "Mein erstes lambda-script!" wird nie erreicht, weil der repeat-Block nicht verlassen wird. Würde er verlassen werden, hätten wir den Eintrag im Protokoll aber die Kopplung der Lampen wäre weg. Der Ausweg ist, den repeat-Block als selbstständigen Thread laufen zu lassen:

thread {
   repeat {
      wait [Flur_Lampe];
      case ([Flur_Lampe] ~ /on/)
         {case ([WZ_Lampe] !~ /on/) {set WZ_Lampe on}}
         {case ([WZ_Lampe] !~ /off/) {set WZ_Lampe off}}
   }
};
log "Mein erstes lambda-script!"

Das Kommando thread {...} startet den dem Symbol thread folgenden Block als ein quasi eigenständiges Programm. Unmittelbar danach wird das Hauptprogramm mit log "Mein erstes lambda-script!" fortgesetzt. Da in der Praxis die Programmstruktur thread {repeat {...}} häufig vorkommt, gibt es dafür das zusammenfassende Kommando: always {...}.

always {
   wait [Flur_Lampe];
   case ([Flur_Lampe] ~ /on/)
      {case ([WZ_Lampe] !~ /on/) {set WZ_Lampe on}}
      {case ([WZ_Lampe] !~ /off/) {set WZ_Lampe off}}
};
log "Mein erstes lambda-script!"

Das nächste Ziel soll sein, diese erfolgreiche Verknüpfung von Flur_Lampe und WZ_Lampe auf ein zweites Paar Lampen (Lampen in Esszimmer und Küche) EZ_Lampe und K_Lampe zu übertragen.

Jetzt bietet es sich an, diese Funktionalität in einer neu zu definierendes Funktion zu kapseln. Das Kommando zum Aufruf dieser Funktion soll so aussehen:

Lampe1 -> Lampe2;

Mit der Existens einer solchen Funktion ließe sich die Kopplung der vier Lampen einfach mit den Kommandos:

Flur_Lampe -> WZ_Lampe;
EZ_Lampe -> K_Lampe;

lösen. Die Definition dieser Funktion geschieht durch folgendes Kommando:

'(fhemDevice'Lampe1 -> fhemDevice'Lampe2) {
   always {
      wait [Lampe1];
      case ([Lampe1] ~ /on/)
         {case ([Lampe2] !~ /on/) {set Lampe2 on}}
         {case ([Lampe2] !~ /off/) {set Lampe2 off}}
   }
};

Flur_Lampe -> WZ_Lampe;
EZ_Lampe -> K_Lampe;

Die erste Zeile enthält die Signatur der zu definierenden Funktion. Die Signatur wird in runde Klammer mit einem vorangestellten Hochkomma geschrieben. Sie besteht aus den Klassennamen der verwendeten Variablen und den im Aufruf verwendeten Symbolen (hier: ->) in genau der Reihenfolge wie sie in dem Kommando stehen sollen. Den Klassennamen folgt jeweils ein Hochkomma und ein Symbol. Dieses Symbol steht im nachfolgenden Block für den im Aufruf verwendeten Wert. Der Signatur schließt sich der Block mit den Kommandos die die Funktion dann ausmachen an.

Die definierte Funktion lässt sich nutzen um zwei Lampen komplett su synchronisieren. Das heisst, eine soll schalten wenn die andere geschaltet wird. Das folgende Script erzeugt dazu eine zweite Funktion mit dem Symbol <->, die unter Verwendung der ersten definiert wird. Der Aufruf dieser Funktion synchronisiert die Lampen EZ_Lampe und K_Lampe:

'(fhemDevice'Lampe1 -> fhemDevice'Lampe2) {
   always {
      wait [Lampe1];
      case ([Lampe1] ~ /on/)
         {case ([Lampe2] !~ /on/) {set Lampe2 on}}
         {case ([Lampe2] !~ /off/) {set Lampe2 off}}
   }
};

'(fhemDevice'Lampe1 <-> fhemDevice'Lampe2) {
   Lampe1 -> Lampe2; Lampe2 -> Lampe1
);

Flur_Lampe -> WZ_Lampe;
EZ_Lampe <-> K_Lampe;
2.2 Rollos öffnen und schließen

Jetzt soll ein λscript geschrieben werden, dass die Rollläden Rollo1, Rollo2 und Rollo3 am Morgen nach dem hell werden - aber nicht vor 6:30 Uhr öffnet. Das Öffnen soll mit einer variablen Verzögerung zwischen 0 und 5 Minuten erfolgen. Die Zeitspanne zwischen dem Öffnen der Rollos soll zwischen 20 und 40 Sekunden variieren. Die Reihenfolge soll ebenfalls zufällig sein. Die Rollos sollen geschlossen werden wenn es draußen dunkel ist - aber spätestens 21:30 Uhr. Die Helligkeit wird über das Reading brightness eines außen angebrachten Bewegungsmelder gemessen. Der Name dieses Devices ist BM_Garten.

Mit dem fhem-Befehl "define rollos lambda" wird das λscript angelegt. Die oben gestelle Aufgabe löst das folgendeScript:

'(moveRollos string'direction) {
   rollos := new stack of fhemDevice;
   rollos << rollo1 rollo2 rollo3;
   wait (random 5)'minutes;
   forEach rollos'shuffle rollo {
      wait (random 20 40)'seconds;
      set rollo direction;
   }
};

repeat {
   case
      ([BM_Garten brightness]'toNumber > 120 & (> 6:30)) {moveRollos "up"}
      ([BM_Garten brightness]'toNumber < 110 | (> 21:30)) {moveRollos "down"}
   };
   wait [BM_Garten brightness] 6:30'today 21:30'today;
}

Das erste Kommando ist wieder die Definition einer Funktion. Diese Funktion öffnet und schließt Rollos in Abhängigkeit vom zweiten Parameter. Hier sind es die Werte "up" bzw. "down". In einer konkreten fhem-Installation kann das auch "on" bzw. "off" oder auch "pct 0" bzw. "pct 100" sein. Das muss dann möglicherweise angepasst werden.

Das erste Kommando im Block rollos := new stack of fhemDevice; definiert ein leeres Array in das Devices eingespeichert werden können. Das passiert in der nächsten Zeile. Das Kommando (random 5) liefert eine ganzzahlige Zufallszahl zwischen 0 und 5. (random 5)'minutes macht daraus eine Zeitspanne (Instanz von timespan). Diese Zeitspanne wird nun gewartet und das Script danach mit dem forEach ...-Kommando fortgesetzt. Wie in anderen Programmiersprachen auch, wird der im forEach ...-Kommando enthaltene Codeblock für jedes Element des Arrays, das dem Symbol forEach folgt, ausgeführt. Mit rollos'shuffle wird eine Funktion aufgerufen, die die Elemente des arrays rollos in eine zufällige andere Reihenfolge bringt. rollo bezeichnet dann innerhalb des Block das jeweils aktuelle Element des Arrays.

Der Block besteht selbst aus nur zwei Kommandos: In wait (random 20 40)'seconds; wird analog oben eine Zufallszahl zwischen 20 und 40 in eine Zeitspanne 20-40 Sekunden umgewandelt und diese Zeit gewartet. Danach wird der übertragene Parameter direction an das entsprechende Rollo übertragen.

Der Funktionsdefinition schließt sich mit dem repeat-Kommando das eigentliche Programm an. Hier sind zwei Funktionen, die erstmalig verwendet werden, zu erklären: [BM_Garten brightness]'toNumber wandelt das Reading brightness in eine Zahl um, damit sie mit dem Schwellwert 110 bzw. 120 verglichen werden kann und (> 6.30) liefert den boolschen Wert true oder false, je nach dem, ob seit dem Beginn des laufenden Tages mehr oder weniger als 6 Stunden und 30 Minuten vergangen sind.

Das letzte Kommando wait [BM_Garten brightness] 6:30'today 21:30'today; stoppt das Script um es fortzusetzen wenn sich entweder die Helligkeit ändert oder die Uhrzeit 6:30 oder 21.30 erreicht werden. Zeitpunkte, die in der Vergangenheit liegen, werden beim Start des wait-Kommandos übersprungen. Wird das wait-Kommando etwa um 7:00 Uhr abgearbeitet, wird der Parameter 6:30'today übergangen und nur auf die eine Veränderung der Helligkeit oder bis maximal 21:30 Uhr gewartet.

2.3 Treppenlicht nachts mehrmals zufällig einschalten

Es soll ein Script erstellt werden, dass zwischen 23:00 Uhr und 4:00 Uhr in einen zufälligen Abstand von 20 bis 40 Minuten den Treppenhauslichtschalter betätigt, um so Anwesenheit und Wachheit zu simulieren.

repeat {
   while [23:00 4:00] {
      wait (random 20 40)'minutes;
      set TreppenhausLicht press short self01;
   };
   wait 23:00'today;
}

Dieses Script ist nach dem bisher gesagten leicht verständlich. Neu ist das while-Kommando. Dem Schlüsselwort while folgt ein boolscher Wert und ein Block. Der Block wir solange ausgeführt wie der boolsche Wert true ist. Der boolsche Wert ist hier [23:00 4:00]. Der wird zu true ausgewertet wenn aktuelle Uhrzeit in dem angegebene Intervall liegt. Ansonsten ist er false und die while-Schleife wird verlassen. Danach wird bis 23:00 Uhr gewartet um von neuem zu beginnen.