Schnelleinstieg mit praktischen Beispielen
Nachfolgend finden sich einige λscript-Beispiele die den Einstieg erleichtern sollen. Zunächst werden kurz die verwendeten Grundbegriffe definiert und danach kleine Scripte entwickelt. Die in den Scripten verwendeten Devices müssen selbstverständlich an die der aktuellen Installation angepasst werden. Die verwendeten Kommandos werden kurz erklärt. Eine genauere Dokumention findet sich in der Sprachbeschreibung.
1 Definition der Grundbegriffe
- Symbol: Ein Symbol ist eine Zeichenfolge, die folgende Zeichen nicht enthält: Leerzeichen, Tabulatoren, Zeilenumbrüche und keines der Zeichen:
[ ] ( ) { } ' " ;
. - Klassen: Eine Klasse ist, wie in allen moderneren Programmiersprachen, ein abstraktes Modell für eine Reihe von ähnlichen Objekten. In λscript können den vordefinierten Klassen Neue hinzugefügt werden. Einfache Vererbung wird unterstützt. Eine Klassendefinition wird durch ihren Namen repräsentiert.
- Werte: λscript ist streng objektorientiert. Werte sind von Klassen abgeleitete Instanzen. Es gibt in λscript vordefinierte Klassen (
number
,string
,timespan
,stack
, ...). Der Wert123
ist zum Beispiel eine Instanz der Klassenumber
, der Wert"Hallo Welt"
ist Instanz der Klassestring
und12:30
eine Instanz der Klassetimespan
. - Variable: Einem Symbol kann ein Wert zugewiesen werden. Dann wird aus diesem Symbol eine Variable. Die Zuweisung geschieht durch ein Kommando mit dem Symbol
:=
. Beispiel:
zahl := 123;
Verwendeten Variablen sind statisch typisiert. Das heißt, jede Variable besitzt einen Wert, der Instanz einer Klasse ist. Dieser Variablen kann als neuer Wert nur ein Objekt derselben Klasse (oder einer davon abgeleiteteten Klasse) zugewiesen werden mit der er erstmalig definiert wurde. Es gibt mit dem Start von λscript vordefinierte Variable. Das sind die Klassennamen (number
,string
,fhemDevice
, ...) und die vorhandenen Devices. D.h. für jedes in der FHEM-Installation existierendes Device gibt es eine Variable mit dem Namen des Devices. Diese Variable ist vom TypfhemDevice
. - Kommando: Ein Kommando ist eine Folge von Symbolen, Werten, Klassennamen und Variablen. Diese Folge ist entweder in runde Klammern eingeschlossen, oder sie beginnt mit dem ersten Symbol und endet mit einem Semikolon. Jedes Kommando ruft eine definierte Funktion auf. Kommandos können ineinander verschachtelt werden. Beispiel:
z := 3 * (4 + 5);
Hier ist das Kommando(3 + 4)
inz := 3 * (4 + 5);
eingebettet. - Block: Ein Block ist eine in geschweifte Klammern eingeschlossene Folge von Kommandos, die nacheinander ausgeführt werden. Beispiel:
{z := 3 * (4 + 5); wait (z seconds); set Lampe on;}
Das letze Semikolon als Abschluss des letzten Kommandos vor der schließenden geschweiften Klammer kann wegelassen werden. - Zugriffsoperator ist in λscript das Apostroph:
'
. Er dient dazu, auf Elemente von Klassen oder Datenstrukturen zuzugreifen. Solche Zugriffe geschehen durch Aufrufe der FormObject'Eigenschaft
.
Die Verwendung des Zugriffsoperators ist lediglich eine andere Schreibweise für Kommandosdie aus einem Wert und einem nachfolgenden Symbol bestehen. Z.B. das Kommando12 hours;
. Es besteht aus einer Zahl (number) und dem Symbol hours
. Es liefert als Ergebis einen Wert vom Typtimespan
. Das Kommando, das das laufende Script stoppt und nach 12 Stunden fortsetzt, lautet:wait 12'hours;
. Das ist so leichter lesbar als:wait (12 hours);
2 Beispielscripts
2.1 Hallo Welt
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.
log "Hallo Welt";
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.
2.2 Lampe einschalten
Gibt es in der aktuellen Installation eine Lampe WZ_Lampe, und enthält das Script diese Zeile
set WZ_Lampe on;
wird sie durch das Starten des Scripts eingeschaltet.
2.3 Lampe ein- und ausschalten
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 oder die beiden Schrägstriche sind zwingend, damit λscript ein Zeichenfolge der Gestalt m/d/h:min,sec
als eine Zeitspanne interpretiert. Die Bestandteile m
, d
, h
, min
müssen ganze Zahlen sein. sec
kann eine Dezimalzahl (mit einem Punkt als Dezimaltrennzeichen) sein. 0,0.2
steht für 200 Millisekunden.
2.4 Lampe mehrfach ein- und ausschalten
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 3
nach dem Symbol repeat
weggelassen, wird der nachfolgende Block unendlich oft ausgeführt.
repeat 3 {
set WZ_Lampe on;
wait 0,10;
set WZ_Lampe off;
wait 1,0;
}
Dieses Script schaltet das Device WZ_Lampe für 10 Sekunden an. Der Vorgang wird nach einer Minute wiederholt. Insgesamt drei mal.
2.5 Lampe zufällig schalten
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 ein von außen sichbares Licht (hier das Device TreppenhausLicht) einschaltet, um so Anwesenheit und wach sein zu simulieren.
repeat {
while '([23:00 4:00]) {
wait (random 20 40)'minutes;
set TreppenhausLicht on;
wait 2,0;
set TreppenhausLicht off;
};
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 Funktional mit einem boolschen 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 die aktuelle Uhrzeit in dem angegebenen Intervall liegt. Ansonsten ist er false
und die while
-Schleife wird verlassen. Danach wird bis 23:00 Uhr gewartet, um von neuem zu beginnen.
Das Kommando (random 20 40)
liefert eine zufällige ganze Zahl zwischen 20 und 40. Die Funktion number minutes
wandelt diese Zahl in einen Wert vom Typ timespan
um. Dieser Wert liegt dann zwischen 20 und 40 Minuten. Diese Zeit wird gewartet und dann das Licht für 2 Minuten eingeschaltet.
2.6 Erweiterte Lampensteuerung
2.6.1 Lampe mit variablem Ausschaltzeitpunkt
Ein Lampe (Device LampeEingang) wird von mehreren Szenarien geschaltet.
- Ein Bewegungsmelder schaltet die Lampe für 3 Minuten ein
- Die Lampe leuchtet dauerhaft von 6:00 Uhr bis 21:00 Uhr
- Bei einer Gartenparty leuchtet sie so lange die Party dauert
- …
Diese Funktionalität soll mit einer neu zu definierenden Klasse clsLamp
realisiert werden:
clsLamp := fhemDevice
onTill moment'empty
;
Die neue Klasse clsLamp
ist von fhemDevice
abgeleitet und hat eine neue Eigenschaft onTill
. Diese Eigenschaft soll den Zeitpunkt aufnehmen, an dem die Lampe automatisch ausgeschaltet werden soll. Das Kommando zum Einschalten bis zu einem gewissen Zeitpunkt (z.B. heute 22:00 Uhr) soll sein: set LampeEingang on 22:00'today
. Dazu wird folgende Funktion definiert:
'(set clsLamp'lampe on moment't) := {
lampe'onTill := max lampe'onTill t;
if ([lampe] ~ /off/) {set lampe on};
thread {
wait lampe'onTill;
if (lampe'onTill <= now) {set lampe off};
};
};
Die erste Zeile der Funktion ist die Signatur: Symbol set
gefolgt vom Device (als Instanz der Klasse clsLamp
), dem Symbol on
und dann die Angabe des Ausschaltzeitpunktes als Wert vom Typ moment
.
Die nächste Zeile berechnet die neue Ausschaltzeit (lampe'onTill
) als dem Maximum aus der alten Ausschaltzeit und der Neuen.
Danach wird die Lampe, wenn sie aus ist, eigeschaltet: if ([lampe] ~ /off/) {set lampe on};
Jetzt wird ein Thread gestartet, ein Programm, das parallel zum dem laufenden Script läuft. Dieser Thread wartet bis zum Ausschaltzeitpunkt und schaltet die Lampe aus. Die if
-Abfrage vor dem Auschalten in der Zeile if (lampe'onTill <= now) {set lampe off};
sichert ab, dass die Lampe, wenn während der Wartezeit eine Verlängerung der Einschaltdauer erfolgt ist, die Lampe nicht vorzeitig auschaltet wird.
Ein Testscript könnte nun so aussehen:
clsLamp := fhemDevice
onTill moment
;
'(set clsLamp'lampe on moment't) := {
lampe'onTill := max lampe'onTill t;
if ([lampe] ~ /off/) {set lampe on};
thread {
wait lampe'onTill;
if (lampe'onTill <= now) {set lampe off};
};
};
new LampeEingang1 clsLamp;
new LampeEingang2 clsLamp;
…
set LampeEingang1 on 14:00'today;
wait 13:55;
set LampeEingang1 on 15:00'today;
set LampeEingang1 on 14:30'today;
Nach den Definitionen der Klasse und der Funktion werden die Lampen, die mit dieser Funktionalität ausgestattet werden sollen, auf die neue Klasse clsLamp
"upgegradet". Probeweise wird nun LampeEingang1
bis 14:00 Uhr eingschaltet. Kurz vor Ablauf der Zeit wird die Dauer noch einmal bis 15:00 Uhr verlängert. Das Kommando set LampeEingang1 on 14:30'today;
bleibt ohne Wirkung.
2.6.2 Lampe mit variabler Einschaltdauer
Die Funktionalität der Klasse clsLamp
soll nun so erweitert werden, dass eine Lampe nicht nur bis zu einem definierten Zeitpunkt, sondern auch für eine gewisse Zeitdauer eingeschaltet werden kann.
Das Kommando zum Einschalten für z.B. 5 Minuten soll sein: set LampeEingang on 5,0
. Dazu wird folgende Funktion definiert:
'(set clsLamp'lampe on timespan't) := {set lampe on (now t)};
Hier wird einfach aus der Zeitdauer die geplante Ausschaltzeit berechnet (jetzt + Dauer
) und die vorher definierte Funktion aufgerufen. Die beiden Funktionen sehen von Ihren Aufruf gesehen zwar ähnlich aus, besitzen aber eine unterschiedliche Signatur. Der letzte Parameter ist einmal vom Typ moment
und hier vom Typ timespan
. Damit sind sie wohlunterschieden.
Das komplette Testscript sieht nun so aus:
clsLamp := fhemDevice
onTill moment
;
'(set clsLamp'lampe on moment't) := {
lampe'onTill := max lampe'onTill t;
if ([lampe] ~ /off/) {set lampe on};
thread {
wait lampe'onTill;
if (lampe'onTill <= now) {set lampe off};
};
};
'(set clsLamp'lampe on timespan't) := {set lampe on (now t)};
new LampeEingang1 clsLamp;
new LampeEingang2 clsLamp;
…
set LampeEingang1 on 1,0;
wait 0,10;
set LampeEingang1 on 14:00'today;
wait 13:55;
set LampeEingang1 on 3,0;
set LampeEingang1 on 14:30'today;
Auch hier ändert der Einschaltbefehl set LampeEingang1 on 3,0;
(Der in dieser Form von einem Bewegungsmelder kommen kann.) nichts daran, das die Lampe bis 14:00 Uhr an.
2.7 Lampen synchron schalten
Angenommen es gibt in der fhem-Installation noch ein Device Flur_Lampe. Ziel soll sein, ein Script zu schreiben, dass WZ_Lampe immer ein- bzw. ausgeschaltet wird, wenn Flur_Lampe ein- bzw. ausgeschaltet wird. Ein Script dafür wäre:
repeat {
wait [Flur_Lampe];
if ([Flur_Lampe] ~ /on/) {set WZ_Lampe on} ([Flur_Lampe] ~ /off/) {set WZ_Lampe off};
}
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. Ist der wait
folgende Parameter keine Zeitangabe, 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 sich mindestens ein der Parameter geändert hat. wait [Flur_Lampe];
wartet also darauf, dass sich der Wert des Readings state von Flur_Lampe ändert. Danach wird mit dem Script fortgefahren.
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 eine Veränderung des Readings state von Flur_Lampe gewartet.
Um sicher zu stellen, das WZ_Lampe nur ein- bzw. ausgeschaltet wird, wenn sie nicht schon an bzw. aus ist, wird das if
-Kommandos erweitert.
repeat {
wait [Flur_Lampe];
if ([Flur_Lampe] ~ /on/ & [WZ_Lampe] !~ /on/) {set WZ_Lampe on}
([Flur_Lampe] ~ /off/ & [WZ_Lampe] !~ /off/) {set WZ_Lampe off};
}
Diese Funktionalität soll nun in einem neu zu definierenden Kommando gekapselt werden. Der Aufruf des Kommandos soll so aussehen:
Flur_Lampe -> WZ_Lampe;
Terasse_Licht -> Teich_Lampe;
Einmal definiert, lässt sich diese Funktion für jede Kombination von Geräten die sychron geschalten werden sollen aufrufen. Z.B. auch für die Kombination Terasse_Licht und Teich_Lampe. Die Funktion besteht im wesentlichen aus den Codezeilen von oben:
'(fhemDevice'Lampe1 -> fhemDevice'Lampe2) := {
always {
wait [Lampe1];
if ([Lampe1] ~ /on/ & [Lampe2] !~ /on/) {set Lampe2 on}
([Lampe1] ~ /off/ & [Lampe2] !~ /off/) {set Lampe2 off};
};
};
Die erste Zeile definiert die sogenannte Signatur der Funktion. Sie besteht aus:
- einer Variablen
Lampe1
der KlassefhemDevice
- dem Symbol
->
- einer Variablen
Lampe2
der KlassefhemDevice
Die gesamte Signatur wird in runde Klammern gesetzt und ein '
vorangestellt. Danach schließt sich ein Kommandoblock an. Der enthält die Kommandos die beim Aufruf der Funktion abgearbeitet werden.
Wichtig ist die Änderung des Schlüsselwortes repeat
in always
. Das hat folgenden Hintergrund: Wird die Funktion aufgerufen bleibt sie in der repeat
->Schleife hängen. Von den Aufrufen Flur_Lampe -> WZ_Lampe;
Terasse_Licht -> Teich_Lampe;
würde nur ersterer gestartet. Mit der Verwendung des Schlüsselwortes always
an Stelle von repeat
wird die Warteschleife als eigener Thread gestartet. D.h.: Die Wiederholung wird angestoßen und danach mit dem auf die Schleife folgenden Programmcode fortgesetzt. In unserem Beispiel läuft dann die Überwachung von Flur_Lampe und Terasse_Licht quasi parallel nebeneinander und unabhängig von "Hauptscript".
Die eben entwickelte Lösung für Synchronisation läuft nur in einer Richtung. Lampe1
schaltet Lampe2
. Der Vollständigkeit soll noch eine Funktion entwickelt werden, bei der jede Lampe die andere mitschaltet. Die Aufrufsyntax soll Lampe1 <-> Lampe2;
sein. Folgende Funktion löst diese Aufgabe:
'(fhemDevice'Lampe1 <-> fhemDevice'Lampe2) := {
Lampe1 -> Lampe2;
Lampe2 -> Lampe1;
};
Die Funktionsweise ist einfach: Die zuerst entwickelte Funktion wird zweimal aufgerufen. Einmal mit Lampe1
als Master und Lampe 2
als Slave und ein zweites Mal umgekehrt. Das komplette Script sieht dann so aus:
'(fhemDevice'Lampe1 -> fhemDevice'Lampe2) := {
always {
wait [Lampe1];
if ([Lampe1] ~ /on/ & [Lampe2] !~ /on/) {set Lampe2 on}
([Lampe1] ~ /off/ & [Lampe2] !~ /off/) {set Lampe2 off};
};
};
'(fhemDevice'Lampe1 <-> fhemDevice'Lampe2) := {
Lampe1 -> Lampe2;
Lampe2 -> Lampe1;
};
Flur_Lampe -> WZ_Lampe;
Terasse_Licht <-> Teich_Lampe;
Hier ist noch wichtig anzumerken, das neue Funktionen immer vor ihrer ersten Verwendung definiert werden müssen.