logoFHEM CONTROL 2.0

Sprachbeschreibung

1 Einleitung

Stand: 08.11.19 08:58


λscript wurde geschrieben um große komplexe Systeme mit vielen Sensoren und Aktoren zu steuern. Übliche "SMART HOME"-Systeme sind vergleichsweise klein: Ein paar Rolläden, einige Thermostate und Ventile, und ein paar Lampen - viel mehr ist es oft nicht. Trotzdem können auch solche Systeme sehr komplex werden. Die Anzahl der möglichen Verküpfungen wächst exponentiell mit der Anzahl der Komponenten. Mit den klassischen Möglichkeiten die FHEM bietet, wird die Programmierung der Steuerung schnell unübersichtlich und fehleranfällig. λscript schafft hier eine Alternative. Moderne Programmiertechniken (insbesondere objektorientierte und funktionale Programmierung) erlauben es sehr komplexe Aufgaben mit wenig Code zu lösen. Komfortables Multithreading rundet die Sprache ab.

λscript ist sehr umfangreich und bietet mehr Möglichkeiten als der durchschnittliche FHEM-Benutzer wahrscheinlich benötigt. Nach der Fertigstellung dieser Seite soll sie den gesamten Sprachumfang dokumentieren. Sie ist eher zum Nachschlagen als zum Erlernen der Sprache gedacht. Es ist kaum möglich, etwas vollständig zu erklären ohne auf später Beschriebenes vorzugreifen. Das macht das Verstehen einiger Textpassagen für den Einsteiger, der noch nie programmiert hat, etwas schwieriger. Für erfahrene Programmierer trifft das sicher weniger zu.

Für den Anfänger ist es einfacher, mit simplen Scripts, wie sie hier z.B. unter dem Menupunkt "Schnellstart" zu finden sind, zu beginnen. Diese können fast spielerisch abgewandelt und ergänzt werden. Das sorgt für ein steile Lernkurve und macht das Verstehen dieser Sprachbeschreibung leichter.

2 Einordnung in die Welt der Programmiersprachen

Stand: 11.06.19 15:05

Wenn man λscript klassifizieren möchte, lässt sich das anhand der in ihr umgesetzten Programmierparadigmen tun. λscript ist:

3 Grundlegende Begriffe

3.1 Symbol

Stand: 26.11.19 19:28

Ein Symbol ist eine Zeichenfolge, die folgende Zeichen nicht enthält: Leerzeichen, Tabulatoren, Zeilenumbrüche und keines der Zeichen: [ ] ( ) { } ' " ;

Einige Symbole werden in λscript für die Definition von Funktionen verwendet. Das sind z.B.:

:= ==
+ - * / ^
= !=
< <= <> <=> => >
~ !~
<< >>
& | !
my new
if wait repeat while forEach loop quit
thread always terminate stop
set setReading
today tomorrow yesterday
year easter month week day hour minute now
years months weeks days hours minutes seconds
random shuffle
class
toString toBoolean toNumber alternatively

Das Zuweisen von Werten oder Klassen an diese Symbole verändert sie zu Variablen. Dann sind die damit definierten Funktionen in ihrer ursprünglichen Form nicht mehr aufrufbar. (Der gleiche Effekt tritt ebenfalls auf, wenn diese Symbole als Namen für ein λscript verwendet werden.) Das stellt kein prinzipielles Problem dar; der Aufruf der Funktionen ist immer noch möglich. Einfacher ist es, die oben genannten Symbole nicht als Variable, Klassennamen oder Devicenamen zu verwenden. Sollte man dies doch tun wollen, ist lediglich zu verhindern, dass beim Aufruf von Funktionen, in denen diese Symbole auftauchen, der Compiler an die Stelle des Symbols, die Variable einsetzt. Das wird durch das Voranstellen eines Apostrophs ' erreicht.

Wenn z.B. wait ein Wert zugewiesen wurde, ist wait damit zu einer Variablen geworden. Dann muss der Befehl für "Warte eine Stunde" als 'wait 1:00; geschrieben werden.

3.2 Klasse

Stand: 18.06.19 11:03

Eine Klasse ist, wie in allen objektorientiert arbeitenden Programmiersprachen, auch hier ein abstraktes Modell für eine Reihe von ähnlichen Objekten. Es gibt in λscript vordefinierte Klassen (number, string, timespan, stack, fhemDevice, ...). Alle Klassen sind eine Instanz der Klasse class. In λscript können den vordefinierten Klassen Neue hinzugefügt werden. Einfache Vererbung wird unterstützt. Eine Klassendefinition wird durch ihren Namen repräsentiert. Mit der Definition einer neuen Klasse entsteht eine neu Variable die mit dem Namen der Klasse identisch ist.

3.3 Werte

Stand: 18.06.19 07:05

λscript ist streng objektorientiert. Alle Werte sind von Klassen abgeleitete Instanzen. Der Wert 123 ist zum Beispiel eine Instanz der Klasse number; oder 123 ist vom Typ number. Der Wert "Hallo Welt" ist Instanz der Klasse string und 12:30 eine Instanz der Klasse timespan.

3.4 Variable

Stand: 06.11.19 15:16

Einem Symbol kann ein Wert zugewiesen werden. Dann wird aus diesem Symbol eine Variable. Die Zuweisung geschieht durch den Zuweisungsoperator :=. Beispiel:

zahl := 123;

Alles was rechts vom Symbol := steht, wird als Kommando interpretiert, ausgeführt und das Ergebnis dem links stehenden Symbol zugewiesen. Steht links bereits ein Wert, wird der durch den neu Berechneten ersetzt.

Alle 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 vom Typ der Klasse zugewiesen werden, mit der die Variable erstmalig definiert wurde.

Es gibt mit dem Start von λscript vordefinierte Variable. Das sind die

3.5 Devices

Stand: 01.08.19 07:27

Unter diesem Begriff wird die zu der Installation gehörende Hardwareumgebung zusammengefasst. In erster Linie sind das bei der Integration in eine fhem-Umgebung die fhem-Devices. Automatisch wird beim Start von λscript zu jedem fhem-Device eine Variable vom Typ fhemDevice mit dem Namen des Devices angelegt. Ein weiteres Device ist die aktuelle Log-Datei. Dort kann mit dem Kommando log Text ausgegeben werden. De fakto ist der außerhalb von λscript laufende Timer ebenfalls ein Teil der Hardwareumgebung und wird bei wait-Kommandos verwendet. Während der Zugriff auf Variable in λscripts eingeschränkt sein kann, sind Devices systemweit verfügbar.

3.6 Kommando

Stand: 18.06.19 07:19

Ein Kommando ist eine Folge von Symbolen und Variablen, und führt eine definierte Funktion aus. Diese Folge ist entweder in runde Klammern eingeschlossen, oder sie endet mit einem Semikolon. Kommandos können ineinander verschachtelt werden. Beispiel:

z := 3 * (4 + 5);

Hier ist das Kommando (4 + 5) in z := 3 * (4 + 5); eingebettet.

Ein Kommando kann einen Rückgabewert haben. Dieser wird nach der Abarbeitung des Kommandos im übergeordneten Kommando an dessen Stelle eingesetzt. Im obigen Beispiel wird das Kommando (4 + 5) ausgeführt und das Ergebnis 9 an dessen Stelle eingesetzt. Dann wird mit der Abarbeitung des Scripts fortgefahren.

3.7 Block

Stand: 18.06.19 07:20

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 letzte Semikolon als Abschluss des letzten Kommandos vor der schließenden geschweiften Klammer ist optional, kann also weggelassen werden. Blöcke können ineinander verschachtelt werden. D.h.: Blöcke können wiederum Blöcke enthalten.

Innerhalb eines Blocks kann auf die in übergeordneten Blöcken definierten Klassen und Funktionen und auf alle Devices zugegriffen werden.

Es werden zwei Arten von Blöcken unterschieden: Kommandoblöcke und Definitionsblöcke. Beides sind in geschweifte Klammern Folgen von Kommandos. Der Unterschied ist folgender:

3.8 Programm

Stand: 01.08.19 07:29

Ein Programm, ein λscript, ist ein Kommandoblock ohne die öffnende und schließende geschweifte Klammer. Diese Klammern werden vom Precompiler eigenständig zugefügt.

Jedes λscript ist in einen Superblock eingebettet. In diesem Superblock werden beim Systemstart alle systemimmanenten Klassen, Funktionen und Devices definiert.

3.9 Zugriffsoperator

Stand: 06.11.19 13:15

Der Zugriffsoperator in λscript ist das Apostroph: '. Er dient dazu, auf Elemente von Klassen oder Datenstrukturen zuzugreifen. Solche Zugriffe geschehen durch Aufrufe der Form Object'Eigenschaft. In anderen Programmiersprachen wird dafür auch häufig der Punkt . oder -> verwendet. Da der Punkt aber Bestandteil eines Devicenamens in FHEM sein kann, wurde hier auf das Apostroph ausgewichen.

Die Verwendung des Zugriffsoperators ist lediglich eine andere Schreibweise für bestimmte Kommandos: Kommandos die aus zwei Elemeten bestehen: Aus einem Wert und einem nachfolgenden Symbol. Z.B. das Kommando 12 hours;. Es besteht aus einer Zahl (number) und dem Symbol hours. Es liefert als Ergebis eine Wert vom Typ timespan. 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);

Das Apostroph als Zugriffsoperator wurde lediglich aus Gründen der kürzeren Schreibweise und der besseren Lesbarkeit eingeführt. Es stellt keine neue Funktionalität bereit. λscript wandelt beim Einlesen des Scripts jeden Ausdruck der Form Wert'Symbol; in (Wert 'Symbol) um - macht daraus also ein "normales" Kommando. Dem entsprechend wird aus

Wert'Symbol1'Symbol2'Symbol3;

beim Einlesen

(((Wert 'Symbol1) 'Symbol2 ) 'Symbol3)

Wie dieses Beispiel zeigt, ist, bei einem mehrstufigen Zugriff auf die Eigenschaften eines Objektes, oder bei der Verwendung bestimmter Funktionen, die Schreibweise mit der Verwendung des Apostrophs besser lesbar.

3.10 Kommentarzeile

Stand: 06.11.19 20:11

Skriptzeilen, die mit einem # (es können Leerzeichen vorangehen) beginnen, werden beim Einlesen des Scripts übergangen.

# Das ist eine Kommentarzeile;

Für eine ausführliche Kommentierung des Scripts kann ein Kommentarblock definiert werden. Die erste Zeile eines Kommentarblocks muss mit /# beginnen. (Leerzeichen können vorangehen.) Der Block endet mit einer Zeile, die mit #/ abschließt. Auch hier können Leerzeichen vorangehen und denen beliebig viele # folgen.

   /###############
   Das ist eine Kommentarblock.
      1. Zeile
      ...
      ...
   ###############/

4 Klassen

Stand: 15.05.19 19:57

Auf eine definierte Klasse kann in dem Block, in dem sie definiert wurde, und in allen untergeordneten Blöcken zugegriffen werden. Da die nun aufgelisteten Klassen auf höchster Ebene definiert wurden, sind sie global verfügbar - wenn sie nicht durch den Nutzer neu definiert wurden.

4.1 Vordefinierte Klassen

4.1.1 value

Stand: 18.06.19 07:37

Die Klasse value ist eine Klasse ohne Eigenschaften und gleichzeitig die Basisklasse aller Werte. value ist - die Urahnin jedes Werts in λscript. Alle anderen Werteklassen sind von value abgeleitet.

Die Klasse value kann nicht instanziiert werden. D.h., es gibt keine Werte oder Objekte von Typ value.

4.1.2 number

Stand: 08.06.19 21:09

Zahlen in λscript sind sämtlich von der Klasse number abgeleitet. Die in einem Script auftauchenden Zeichenfolgen 123, 3.14, -12e-5 werden in der üblichen Art und Weise als Zahlen interpretiert.
Besonders bei λscript ist, das auch gemeine Brüche z.B -2/7 als Zahlen erkannt werden. Mit solchen Brüchen wird entsprechend der Regeln der Bruchrechnung exakt und ohne Rundungsfehler gerechnet.

4.1.3 string

4.1.3.1 word

Stand: 07.11.19 12:04

Die Klasse word ist eine von der Klasse string abgeleitete Klasse. Ein Objekt von Typ word> kann eine leere Zeichenkette oder eine Zeichenketten die kein Leerzeichen (und keinen Tabulator oder Zeilenumbruch) enthält sein. Analog string werden sie in einem Script in Anführungszeichen eingeschlossen. So werden "" und "Hallo" automatisch als Zeichenketten vom Typ word erkannt.

Stand: 18.06.19 08:59

Eine Instanz der Klasse string ist eine beliebig lange Zeichenkette. In einem Script werden Zeichenketten in Anführungszeichen " eingeschlossen. So ist "Hallo Welt" eine Zeichenkette.

Soll ein Anführungszeichen in einer Zeichenkette enthalten sein, ist ihm das Zeichen \ voranzustellen. Das Kommando zur Ausgabe von Goethe schrieb "Faust"! in die Log-Datei lautet demnach:

log "Goethe schrieb \"Faust\"!";
Goethe schrieb "Faust"!

4.1.4 regExpr

Stand: 05.11.19 12:00

Diese Klasse dient zur Aufnahme von regulären Ausdrücken. Ein regulärer Ausdruck ist eine Zeichenkette, die der Beschreibung von Mengen von Zeichenketten mit Hilfe bestimmter syntaktischer Regeln dient. Reguläre Ausdrücke finden vor allem in der Softwareentwicklung Verwendung.

Beispiele für regulären Ausdrücke in λscript sind:

m/on/
m/^set_/

Das vorangestellte m ist in beiden Fällen optional. Mit Hilfe diese Werte kann getestet werden, ob ein Zeichenkette die Zeichenfolge on enthält oder mit der Zeichenfolge set beginnt. Enthält die Variable x eine Zeichenkette erfolgt der Test unter Verwendung des Symbols ~:

isOn := x ~ m/on/;

oder

isOn := x ~ /on/;

Beginnt der reguläre Ausdruck mit einem s werden die untersuchten Zeichenketten, entsprechend der im regulären Ausdruck codierten Vorschrift, modifiziert.

s/on/off/g
s/^set_//

Mit Hilfe diese Werte wird in der Zeichenkette die Zeichfolge on durch off ersetzt bzw. die führende Zeichenfolge set_ entfernt.

Über die Arbeitsweise von regulären Ausdrücken gibt es eine Vielzahl von Dokumentationen. (z.B: https://wiki.selfhtml.org/wiki/Regulärer_Ausdruck ) Deshalb wird sie an dieser Stelle nicht vertieft.

4.1.5 measurement

Stand: 10.12.19 17:16

Diese Klasse modelliert die Messwerte physikalischer Größen. Ein Messwert besteht immer aus einer Maßzahl und einer Maßeinheit. Neben dem Zahlenwert, der vomTyp number ist, hat measurement eine weitere Eigenschaft: die Maßeinheit. Die Initialisierung einer Variablen l vom Typ measurement, die den physikalischen Wert "10 m" enthalten soll, erfolgt deshalb nach folgendem Schema:

l := [10 m];

Ein Kommando bestehend aus eckiger Klammer auf, der Maßzahl von Typ number, einem Symbol für die Maßeinheit und eckiger Klammern zu, liefert einen Wert vom Typ measurement. Als Maßeinheiten sind alle SI-Einheiten (inkl. der gesetzlich zulässigen Vorsätze) erlaubt. Daneben sind auch einige nicht-SI-Einheiten zulässig: °C, °F, ... Eventuell notwendige Exponenten werden (wenn nötig mit Vorzeichen) hinter der Maßeinheit notiert.

v := [10 m3];

Damit hat die Variable v den Wert 10 m3. Das Kommando

g := [9.81 m1s-2];

definierte eine Variable g mit dem Wert 9.81 m/s2.

Einer Variable vom Typ measurement kann nach ihrer Initialisierung nur wieder ein Wert mit der gleichen physikalischen Dimension zugewiesen werden. Soll der oben definierten Variablen l, die die Dimension Länge repräsentiert, der Wert "12 Sekunden" (Dimension Zeit) zugewiesen werden, führt die Anweisung l := [12 s]; zu einer Fehlermeldung.

Mit Werten vom Typ measurement kann wie mit Zahlen gerechnet werden. Bei Multiplikation und Division werden außer neben den Maßzahlen auch die Maßeinheiten miteinander verknüpft. Addition und Subtraktion sind nur möglich, wenn beide Operanden von der selben physikalischen Dimension sind. In dem folgenden Beispiel werden 1 m und 1 mm addiert.

l1 := [1 m];
l2 := [1 mm];
l3 := l1 + l2;
log l3;
1.001 m

Wie man an diesem Beispiel sieht, wird die Operation unter Berücksichtigung der konkreten physikalischen Einheit durchgeführt. Möchte man die Ausgabe in einer anderen Einheit (z.B. in cm) haben, wird dem Ergebnis die gewünschte Einheit zugewiesen.

l1 := [1 m];
l2 := [1 mm];
l3 := l1 + l2;
log l3;
1.001 m
l3 unit cm;
log l3;
100.1 cm

Der Versuch der Zuweisung einer nicht passenden Einheit führt zu einer Fehlermeldung.

4.1.6 boolean

Stand: 07.12.19 09:46

Von der Klasse boolean sind genau zwei Werte instanziierbar: true und false.
Will man z.B. eine Variable t mit dem Wert true, und eine Variable mit dem Wert false belegen, notiert man im Script:

t := true;
f := false;

4.1.7 moment

Stand: 01.08.19 07:34

Die Klasse moment repräsentiert Objekte, die einen bestimmten Zeitpunkt definieren. Intern wird dieser Zeitpunkt durch die entsprechende Unix-Zeit festgelegt. (Die Zahl der vergangenen Sekunden seit Donnerstag, dem 1. Januar 1970, 00:00 Uhr UTC.)

Konkrete Zeitpunkte sind: Der Beginn und das Ende eines Fußballspiels, oder der Moment in dem der erste Mensch den Mond betreten hat.

Ein fester Zeitpunkt kann z.B. durch die Angabe von Tag, Monat und Jahr definiert werden:

m := 31.1.2019
log m;
28.2.2019 00:00,00

Oder mit Uhrzeit:

m := 31.1.2019 12:34,56
log m;
28.2.2019 12:34,56

Zu letzten Beispiel ist anzumerken, dass es sich hier im Grunde um die Addition eines Objekts moment 31.1.2019 und eines Objekts timespan 12:34,56 handelt.

In λscript gibt es eine Reihe von Funktionen mit denen ein solcher bestimmter Zeitpunkt erzeugt werden kann. Dazu zählt z.B. die Einwort-Funktionen now. Das Kommando t := now; weist der Variablen t die aktuelle Zeit zu.

Analog arbeiten z.B. die Funktionen today und hour. Nach der Abarbeitung des Kommandos

t := today;
h := hour;

hat t den Wert "heute 0:00 Uhr" - den Zeitpunkt des Beginns des aktuellen Tages und h den Wert des Beginns der laufenden Stunde.

4.1.8 timespan

Stand: 18.06.19 09:20

Die Klasse timespan repräsentiert Objekte, die eine bestimmte Zeitspanne beschreiben. Eine Zeitspanne ist die Differenz zwischen zwei Objekten von Typ moment. So ist die aktuelle Uhrzeit ein timespan und genau die Zeitspanne vom Beginn des heutigen Tages bis jetzt. Konkrete Zeitspannen sind auch: Die 90 Minuten Spieldauer eines Fußballspiels, oder die Zeit die man braucht, um von der Erde bis zum Mond zu fliegen.

Im Script auftauchende Zeichenketten der Gestalt m/d/h:min,sec werden als eine Zeitspanne von m Monaten, d Tagen, h Stunden, min Minuten und sec Sekunden interpretiert. (sec kann ein mit Punkt geschriebener Dezimalbruch sein.) Dabei ist die Verwendung des Doppelpunktes, des Kommas oder die Verwendung von zwei Schrägstrichen zwingend notwendig, damit λscript die Zeichenfolge als timespan interpretiert. Beispiele:

4.1.9 stack

Stand: 11.10.19 21:10

Ein Objekt von Typ stack ist eine geordnete Menge von Objekten. Objekte können oben auf den Stapel gelegt werden oder unter den Anfang des Stapels geschoben werden. Der lesende und der löschende Zugriff ist nur auf das oberste oder das unterste Objekt möglich.

stack hat eine Eigenschaft of. Diese Eigenschaft definiert die Klasse, der alle Objekte des Stapel angehören müssen. (Instanzen von davon abgeleiteten Klassen selbstverständlich eingeschlossen.) Der Standardwert von of ist value. Damit kann ein mit s := new stack definierter Stapel sämtliche Werte aufnehmen, da alle Objekte in λscript Abkömmlinge von value sind.

Eine Stapel s der nur Zahlen enthalten kann, wird mit

s := new stack of number;

definiert.

Auf diesen leeren Stapel können mit dem Kommando

s << 3 4;

die Zahlen 3 und 4 obendrauf gepackt werden. Das Kommando

1 2 >> s;

schiebt die Zahlen 2 und 1 unter den Stapel.

log s;

würde jetzt 1 2 3 4 ausgeben.

Das Auslesen eines Stacks funktioniert ähnlich. Die Kommandos

top := s >;
bottom := < s;

weisen den Variablen top und bottom den obersten bzw. untersten Wert des Stapels zu.

Die Kommandos

top := s >>;
bottom := << s;

bewirken dasselbe, aber das unterste bzw. oberste Element wird zusätzlich vom Stapel gelöscht.

Um alle Elemente eines Stacks in einer Schleife zu durchlaufen, dienen Kommandos mit dem Schlüsselwort forEach (siehe auch hier):

s := new stack of word;
s << "Montag" "Dienstag" "Mittwoch" "Donnerstag" "Freitag";
n := 1;
forEach s d {
   log "Der " n ". Tag ist " d;
   n'+
}

4.1.10 collection

Stand: 12.06.19 10:55

Ein Objekt von Typ collection ist eine indizierte Menge von Objekten. Als Indizes dienen Objekte von Typ word oder Symbole.

collection hat wie stack eine Eigenschaft of. Sie definiert ebenfalls die Klasse, der alle Objekte des collection angehören müssen. Der Standardwert von of ist auch hier value.

Eine Collection c in der nur Zeichenketten gespeichert werden, wird mit

c := new collection of string;

definiert. Das Einspeichern von Werten geht dann so:

c'x := "Hallo";
c'y := " ";
c'z := "FHEM";
log c'x c'y c'z;
Hallo FHEM

Das Kommando c keys; gibt die aktuell in der collection c enthaltenen Indizes in einem Stack vom Typ "stack of word" zurück. Dem entsprechend liefert

c'x := "Hallo";
c'y := " ";
c'z := "FHEM";
log c'keys;
(word|x,z,y)

Dabei ist anzumerken, dass die Reihenfolge der Schlüsselwerte nicht determiniert ist.

4.1.11 vector

Stand: 16.06.19 09:29

Ein Objekt von Typ vector ist ebenfalls eine indizierte Menge von Objekten. Als Indizes dienen die natürliche Zahlen 1, 2, 3, ...

vector hat eine Eigenschaft dimension. Der Wert von dimension muss eine natürliche Zahl sein. Er definiert den höchsten erlaubten Index. Der muss zwingend bei der Definition eines Objekte vom Typ vector angegeben werden. Eine zweite Eigenschaft ist default. Das der Wert, den jedes Element des Vektors hat, solange ihm kein Anderer zugewiesen wurde. default legt auch den Typ der in dem Vektor gespeicherten Daten fest. Ist default z.B. vom Typ string dürfen nur Zeichenketten in dem Vektor gespeichert werden.

Ein Vektor v mit zehn Elementen, die zunächst alle den Wert 1 haben, wird mit

v := new vector dimension 10 default 1;

definiert. In diesen Vektor können dann auch nur Werte vom Typ number gespeichert werden.

4.1.12 fhemReading

Stand: 08.11.19 08:42

Ein Objekt von Typ fhemReading hat zwei Eigenschaften: val und time.

val ist vom Typ string und time vom Typ moment. Sie stehen für den Wert und den Zeitpunkt der letzten Änderung des Readings eines FHEM-Devices.

Ist R eine Variable die ein Objekt vom Typ fhemReading enthält, dann ist R'val der Inhalt des Readings und R'time der Zeitpunkt der letzten Änderung.

Auf ein Reading kann nur lesend zugegriffen werden.

4.1.13 fhemDevice

Stand: 08.11.19 09:04

Ein Objekt von Typ fhemDevice bildet ein Device der FHEM-Installation ab. Objekte dieses Typs können innerhalb eines λscripts nicht instanziiert werden, da sie quasi die unveränderliche Hardwareumgebung abbilden. Beim Systemstart wird für jedes gefundene Device eine Variable vom Typ fhemDevice oder einer passenden definierten Unterklasse angelegt. Der Name der Variablen stimmt mit dem Namen des Devices überein. Existiert also ein Device WZ_Lampe, dann wird eine Variable WZ_Lampe vom Typ fhemDevice angelegt. Ist WZ_Lampe ein Homematic-Gerät ist WZ_Lampe vom Typ fhemHomematicDevice

WZ_Lampe'name liefert den Namen des Devices als ein word-Objekt.

Eine wichtige Eigenschaft eine Objektes dieses Typs ist: readings. Diese Eigenschaft enthält den aktuellen Zustand des Devices. Es handelt sich dabei um ein Objekt vom Typ collection of fhemReading.

Den Wert des Readings level von WZ_Lampe erhalte ich demzufolge durch:
l := WZ_Lampe'readings'level'val;

Da auf die Werte von Readings in der Praxis häufig zugegriffen wird, gibt es dafür unter Verwendung eckiger Klammern die alternative Schreibweise:

level := [WZ_Lampe level];

Ist der Name des Readings state, reicht die Schreibweise:

state := [WZ_Lampe];

Danach stehen in level der Wert des Readings level und in state der Wert des Readings state von WZ_Lampe. Beides sind Objekte vom Typ string.

4.1.13.1 fhemLambdaDevice

Stand: 07.12.19 09:58

Ein fhemDevice, das ein lambda-Script enthält (TYPE: lambda), gehört automatisch zu dieser Klasse. Es kann seine eigenen Readings setzen, so wie jedes Device seinen internen Zustand verwaltet. Das Kommando zum Setzen eines Readings lautet:

setReading [name] [wert] ([name] [wert]);

Dabei steht [name] für den Namen des Readings und [wert] für den zu setzenden Wert. [name] kann ein Symbol oder ein Objekt von Typ word sein. Die dem ersten Parameterpaar folgende Klammer symbolisiert, das beliebig viele weiter Parameterpaare folgen können. Beispiel für das Setzen der Readings temperature und door:

setReading
   temperature 12
   door "closed"
;

Da der Wert eines Readings immer ein string sein muss, wandelt das Kommando setReading alle Werte (hier die Zahl 12) automatisch unter Verwendung der Funktion toString in eine Zeichenkette um. Siehe: Typumwandlungen

4.1.13.2 fhemHomematicDevice

Stand: 08.11.19 13:57

Ein fhemDevice das ein Homematic-Gerät (TYPE: CUL_HM) verwaltet, gehört automatisch zu dieser Klasse.

Hier sind Kommandos möglich, die spezifische für diese Geräteklasse sind. Z.B. Kommandos der Form:

device'channel_??

Dieses Kommando gibt das Gerät dass dem angegebenen Channel von device entspricht zurück. Existiert der Kanal nicht, bricht der Compiler mit einer Fehlermeldung ab. Ist TH-Bad ein Thermostat vom Typ "HM-TC-IT-WM-W-EU" denn liefert das folgende Kommando die aktuelle Luftfeuchtigkeit aus dem Weather-Kanal des Thermostats als Zahl in die Variable humidity.

humidity := [TH-Bad'channel_01 humidity]'toNumber;

4.1.14 astronomy

Stand: 16.09.19 16:13

Ein Objekt von Typ astronomy dient dazu, astronomische Daten bereitzustellen. Mit

mySun := new astronomy;

wird eine Variable mySun angelegt. Dabei werden der Readings latitude und longitude aus dem Device global herangezogen. Die Ausdrücke

azimut := mySun'azimut;
elevation := mySun'elevation;

liefern dann denn aktuellen Sonnenstand in ° (Typ: number).

nextSunrise := mySun'sunrise;
nextSunset := mySun'sunset;

liefern die Zeitpunkte (Typ: moment) des nächsten Sonnenauf- bzw. untergangs.

4.2 Typumwandlungen

Stand: 01.08.19 07:40

In λscript werden Objekte in der Regel nicht automatisch in andere Objektypen transformiert. Es gibt eine einzige Ausnahme: Die besteht in der Konvertierung eines Wertes vom Typ word in den Typ string bei einer Wertzuweisung an ein Objekt das als string deklariert wurde. Soll heißen:

t := "Hallo Welt!";
   ...
t := "lambda";

In diesem Beispiel wird eine Variable t von Typ string initialisiert. Der soll später ein Wert von Typ word zugewiesen werden. In diesem, und nur in diesem, Fall wird der Compiler eine automatische Typumwandlung vornehmen und den Übersetzungsprozess nicht mit einer Fehlermeldung beenden.

Für die Umwandlung eines Wertes in eine Wert eines anderen Typs sind einige Funktionen eingebaut. Für die anderen Fälle muss der User weitestgehend selbst sorgen. Für die Durchführung der Umwandlung eignen sich Kommandos der Gestalt:

new := old toClassname;

oder gleichwertig

new := old'toClassname;

old ist hier ein Wert, ein Repräsentant einer bestimmten Klasse. Dieser Wert wird in eine Variable new vom Typ classname umgewandelt.

4.2.1 Umwandlung nach string

Stand: 07.07.19 16:09

Für alle Klassen, gibt es eine Funktion toString, die die Werte dieser Klassen in den Typ string umwandelt. Diese Funktion wird automatisch aufgerufen, wenn ein Wert bei Ausgabekommandos (log, setReading, ...) ausgegeben werden soll.

n := 123;
s := n toString;

Diese Kommandos weisen der Variablen s die Zeichenkette "123" zu.

Für neue Klassen, die der User selbst erstellt hat, wird automatisch eine entsprechende Funktion generiert. Diese vordefinierten Funktionen können durch eigene Funktionen ersetzt und so an die jeweiligen Bedürfnisse angepasst werden.

Für die Klasse number gibt es eine Besonderheit: Sie enthält eine Eigenschaft format von Typ string. Dieser Eigenschaft kann ein Formatstring zugewiesen werden, mit der die Umwandlung in einen Strings beeinflusst wird. Dieser Formatstring ist der, den man in der Programmiersprache Perl zur formatierten Ausgabe von Zahlen verwendet. Für eine ausführliche Darstellung der Möglichkeiten gibt es zahlreichen Quellen (z.B. https://perldoc.perl.org/functions/sprintf.html). Wird dieser Formatstring einer Zahl zugewiesen, wird die Zahl bei der Umwandlung in einen String entsprechend formatiert:

pi := 3.14159265359;
pi'format := "%.3f";
log "gerundet: " pi;
gerundet: 3.142
pi'format := "";
log "original: " pi;
original: 3.14159265359

In diesem Beispiel wurde die Variable pi zunächst auf 3 Dezimalstellen gerundet ausgegeben. Nach dem Löschen des Formatstrings erfolgt die Ausgabe des originalen Wertes. Die Angabe eines Formatstrings beeinflusst also nicht die interne Darstellung der Zahl, sondern wirkt sich nur bei der Umwandlung in einen String aus.

4.2.2 Umwandlung nach boolean

Stand: 14.06.19 10:53

Neben toString gibt es eine weitere für alle Klassen definierte Typumwandlungsfunktion: toBoolean. Der Aufruf

y := x toBoolean;

setzt y auf den Wert false wenn:

In allen anderen Fällen setzt das Kommando y auf den Wert true.

4.2.3 Umwandlung von string nach number

Stand: 11.10.19 20:48

In der Praxis kommt es häufig vor, dass man Zeichenketten, die Zahlen darstellen, in echte Zahlen (Werte vom Typ number) umwandeln möchte. Dafür gibt es das Kommando toNumber

n := "123";
log n'toNumber;
123

Eine leere Zeichenkette gibt den Wert 0 zurück. Das Script bricht mit einer entsprechenden Fehlermeldung ab, wenn s nicht die String-Darstellung einer Zahl ist. Ob das der Fall ist, lässt sich mit einer Funktion prüfen:

n := "123";
log n'isNumber;
true

s := "12 m";
log s'isNumber;
false

Für den Fall, dass man nicht sicher ist, ob der String eine Zahl darstellt, möchte man vielleicht einen Standardwert zurückgeben. Das wird mit folgendem Kommando erreicht:

s := "12 m";
log (s toNumber alternatively 123);
123

Hier wird jetzt 123 zurückgegeben.

4.3 Definition neuer Klassen

Stand: 13.10.19 18:31

Eine neue Klasse kann mit folgendem Kommando definiert werden:

newClass := oldClass
   Eigenschaft1 Wert1
   Eigenschaft2 Wert2
   ...
   { Initialisierungsblock }
;

newClass ist der Name der neuen Klasse. oldClass ist der Name der Klasse von der die neue Klasse sämtliche Eigenschaften erbt. Für eine komplett neue Klasse ist für oldClass value - die Mutter aller Werteklassen - einzusetzen.

Danach folgt eine Liste der Eigenschaften, die die neue Klasse, zusätzlich zu denen von oldClass, haben soll. Dem Namen der Eigenschaft folgt ein initialer Wert oder ein Klassenname. Ist z.B. Wert1 die Instanz eine bestimmten Klasse, ist damit auch, wie bei einer Variablen, der Typ der Eigenschaft festgelegt. Wert1 kann aber auch der Name einer Klasse sein. Dann muss Eigenschaft1 beim Instanziieren eines Repräsentanten der neuen Klasse einen entsprechenden Wert bekommen.

Nach der der Angabe der Eigenschaften kann ein Block (hier als Initialisierungsblock bezeichnet) folgen. Die Angabe des Blocks ist optional. Ist er bei der Definition der Klasse angegeben worden, dann werden die Kommandos des Blocks bei jeder Instanzierung eines Objektes der neu definierten Klasse abgearbeitet. Innerhalb dieses Blocks existiert eine Variable my, die den Wert des aktuellen Objektes enthält. wurde bei der der Definition der ElternKlasse oldClass ebenfalls ein Initialisierungsblock definiert, wird dieser zuerst abgearbeitet.

Ein Beispiel:

clsShutter := fhemDevice
   direction number
   shading 50
;

Hier wird eine neue Klasse, mit denen Rollläden gesteuert werden sollen, definiert. Die neue Klasse clsShutter hat dann alle Eigenschaften von fhemDevice, plus zwei Weitere, mit denen eine Beschattung gesteuert werden soll: direction - eine Zahl die die Himmelsrichtung angibt in die das zugehörige Fenster zeigt und shading - das Level auf das der Rollladen abgesenkt wird, wenn alle Voraussetzungen für ein notwendige Beschattung erfüllt sind. Der Wert für shading wird auf 50 % vordefiniert der Wert für direction muss bei der Instanziierung für jedes Rollo mit angegeben werden. (siehe hier)

Innerhalb des Blocks, in dem eine neue Klasse definiert wurde, kann die Definition um weitere Eigenschaften ergänzt werden. Dazu werden hinter dem Klassennamen die Eigenschaftsnamen und die entsprechenden initialen Werte notiert - völlig anlog zu Definition der neuen Klasse. Auch die Angabe eines weiteren Initialisierungsblocks ist optional möglich.

newClass
   Eigenschaftx Wertx
   Eigenschafty Werty
   ...
   { Initialisierungsblock }
;

Diese Möglichkeit ist wichtig, wenn zwei Klassen definiert werden sollen, und jede eine Eigenschaft vom Typ der anderen Klasse enthalten soll. Dazu ein Beispiel:

Es soll eine Klasse clsRoom definiert werden. Diese Klasse soll unter anderem eine Eigenschaft shutters enthalten. Diese Eigenschaft ist eine Auflistung aller Rollos in einem Raum. Ein Raum sollen Instanz einer Klasse clsRoom sein. Die Klasse clsShutter soll eine Eigenschaft room besitzen die auf den Raum verweist in dem sich das Rollo befindet. Beide Klassen "in einem Zug" definieren zu wollen, ist nicht möglich, da bei der Definition der ersten Klasse clsShutterauf die ZweiteclsRoom noch nicht zugegriffen werden kann. Deshalb legt man die erste Klasse zunächst ohne die noch nicht zu definierende Eigenschaft an, und ergänzt diese nach der Definition der zweiten Klasse. Für obiges Beispiel könnte das wie folgt aussehen:

clsRoom := value name string;
clsShutter := fhemDevice room clsRoom direction number shading 50;

clsRoom shutters (stack of clsShutter);

In der ersten wird die Klasse clsRaum, nur mit der Eigenschaft name definiert. Die zweite Zeile enthält die Definition der Klasse clsShutter analog oben und der zusätzlichen Eigenschaft room. Jetzt, da dem System die Klasse clsShutter bekannt ist, kann diese bei der Erweiterung der Klasse clsRoom verwendet werden. Dieser Klasse wird jetzt eine Eigenschaft shutters zugefügt, die für einen Stapel von Werten des Typs clsShutter steht.

5 Variable

Stand: 06.11.19 15:22

Variable entstehen, indem man einem Symbol eine Wert zuweist. Jeder Wert ist von einer Klasse abgeleitet. Variablen sind statisch typisiert. Das heißt: Einer Variablen kann als neuer Wert grundsätzlich nur ein Objekt der gleichen Klasse zugewiesen werden, mit der sie definiert wurde. Die Kommandofolge:

zahl := 123;
...
zahl := "Hallo";

würde vom System mit einer Fehlermeldung quittiert.

5.1 Direkte Definition einer Variablen

Stand: 06.11.19 15:55

Für einige Klassen (number, boolean, string, word, timespan) ist die Definition einer Variablen durch ein bestimmtes Zeichenmuster im Script möglich:

zahl := 123;
text := "Hallo Welt";
wort := "Hi";
pause := 1:30,45;
offen? := false;

Das ist weitgehend selbsterklärend. Die Definition von Ausdrücken des Typs timespan ist hier beschrieben. Genau so einfach ist die Definition einer Variablen indem man ihr den Wert einer anderen Variablen oder eines Ausdrucks zuweist.

zahl := 3 + 12 ^ 2;
text := "Ergebnis: " zahl'toString;
wort := 5 * "Hi" ;
uhrzeit := now - today;
offen? := 3 > 5;

5.2 Variable sind Zeiger

Stand: 08.11.19 12:01

Das Prinzip der Speicherung von Objekten in einer Variablen ist im Grunde simpel: Bei der Zuweisung eines Wertes an ein Symbol wird ein leere Speicherzelle gesucht. An dieser Stelle wird das Objekt gespeichert und die Position der Speicherzelle im Symbol abgelegt. Dadurch wird das Symbol zu einer Variablen.

Ein Beispiel: Bei der Initialisierung des Symbols x mit dem Wert "Hallo" wird dem Symbol x ein Speicherplatz zugewiesen. Hier zum Beispiel der Platz mit der Nummer 178.

x := "Hallo";

Schematisch stellt sich die Situation so dar:

Variable Position Inhalt
... 177 ...
x 178 Hallo
... 179 ...

Wird der Variablen danach ein neues Objekt zugewiesen wird dieses Objekt an die Stelle geschrieben auf die die Variable zeigt. Genauer betrachtet liegen in diesem Konzept einige bedenkenswerte Sonderfälle versteckt. Die treten auf, wenn die Objekte, die einer Variablen zugewiesen werden sollen, Eigenschaften von anderen Objekten oder Werte anderen Variablen sind.

5.2.1 Zuweisung einer Variablen an eine Andere

Stand: 08.11.19 12:16

Wird dem Symbol y der Wert von x mit y := x; zugewiesen muss y danach auf eine andere Speicherposition als x weisen, und auf der Speicherposition von y muss der gleiche Wert wie bei x stehen.

x := "Hallo";
y := x;
Variable Position Inhalt
... 177 ...
x 178 Hallo
y 179 Hallo
... 180 ...

Das bedeutet, dass bei der Zuweisung einer Variablen x an eine Variable y ein Kopie des Wertes von x erstellt und diese dann y zugeordnet werden muss. Anderenfalls würden sowohl x als auch y auf den Speicherplatz 178 verweisen. Das hätte zur Folge, das jede Änderung von x in gleichem Maß auch y betreffen würde.

Dieses Verhalten ist im Allgemeinen nicht erwünscht. Deshalb wird in λscript bei der Zuweisung einer Variablen an eine Variable eine Objektkopie erstellt und diese mit der Zielvariablen verknüpft. Um den Kopieraufwand zu minimieren, wird das Kopieren eines Objektes als sogenannte flache Kopie realisiert. Erst wenn ein Schreibzugriff auf ein kopierte Eigenschaft stattfindet, wird die Kopie dieser Eigenschaft vollzogen. Dadurch wird sichergestellt, dass diese Änderung nur die Kopie erreicht und gleichzeitig unnötige Kopiervorgänge vermieden werden.

Ohne diese Vorgehensweise würde die Kommandofolge:

x := "Hallo";
y := x;
x := "Welt";
log y;
Hallo
log x;
Welt

dazu führen, dass beide Variable den Wert Welt hätten.

5.2.2 Zuweisung der Eigenschaft eines Objeks an eine Variable

Stand: 08.11.19 12:29

Im Abschnit Umwandlung nach string wird erläutert, das Werte vom Typ number eine Eigenschaft format besitzen mit der die Formatierung bei der Ausgabe der Zahlen gesteuert wird. Standardmäßig ist diser String leer. Im nachfolgenden Beispiel wird diese Eigenschaft (von Typ string) an eine Variable s übergeben, geändert und die Zahl ausgegeben. Ziel ist, die Zahl auf drei Nachkommastellen zu runden:

pi := 3.14159265359;
s := pi'format;
s := "%.3f";
log "gerundet: " pi;
3.14159265359

Das Script zeigt nicht das erwartete Verhalten: pi wird nicht gerundet auf 3 Nachkommastellen ausgegeben. Nach dem oben Ausgeführten ist das Ergebnis erklärbar. pi'format und s zeigen nicht auf die selbe Speicherzelle. Deshalb wirkt sich die Änderung von s im Kommando s := "%.3f" nicht auf die Eigenschaft format in pi aus. Die schematische Speicherkonfiguration sieht also so aus:

Variable Position Inhalt
... 212. ...
pi 213 3.14159265359
p'format 214
s 215 %.3f
... 216 ...

Um zu Erreichen, dass das Script wie gewünscht funktioniert, muss erzwungen werden, dass s auf den selben Speicherplatz zeigt wie pi'format. In λscript gibt es dafür den Operator ==. Der bewirkt, das das links von ihm stehende Symbol zu einer Variablen wird, die auf den rechts stehenden Wert zeigt. Die beiden Terme sind praktisch, bis auf den Namen, identisch - mehr als nur gleich. Mit der Verwendung dieses Operators wird das erwartete Resultat erzielt:

pi := 3.14159265359;
s == pi'format;
s := "%.3f";
log "gerundet: " pi;
3.142

Jetzt sieht die schematische Speicherkonfiguration so aus:

Variable Position Inhalt
... 212 ...
pi 213 3.14159265359
s, p'format 214 %.3f
... 215 ...

5.3 Definition durch Instanziierung von Klassen

Stand: 07.11.19 13:49

Variable können direkt von Klassen abgeleitet werden. Charakteristisch dafür ist die Verwendung des Schlüsselwortes new. Hier gibt es mehrere Möglichkeiten:

5.4 Definition durch Zuweisung eines Funktionsaufrufs

Stand: 21.11.19 15:07

Eine Besonderheit der funktionalen Programmierung ist, das Variable für einen Funktionsaufruf stehen können. Das geschieht in der Weise, dass einem Symbol ein Funktionsaufruf zugewiesen wird. Diese Variable kann, wie jede andere Variable, in Kommandos verwendet werden. Mit dieser Variablen kann gerechnet und sie kann mit anderen Variablen verglichen werden. Sie kann als Parameter in Funktionsaufrufen dienen. Von außen ist ihr nicht anzusehen, dass sie keinen festen Wert hat. Einen festen Wert bekommt sie erst dann, wenn er für eine Kommunikation mit der Hardwareperipherie des Systems benötigt wird. Z.B. wenn der Wert für die Ansteuerung eines Devices oder die Festlegung eines Wartezustandes benötigt wird.

Das folgende Beispiel soll das demonstrieren:

x := '(random 10 20);
log x;
log x;
log x;

Das Kommando random 10 20; ruft ein Funktion auf, die eine ganzzahlige Zufallszahl zwischen 10 und 20 liefert. Dieser Funktionsaufruf wird dem Symbol x zugewiesen. (Das Apostroph verhindert die sofortige Ausführung des Kommandos. Genau so, wie ein Symbol ein Symbol bleibt wenn ihm ein Apostroph vorangestellt ist, verhindert ein Apostroph vor der Liste die Abarbeitung des in der Liste stehenden Kommandos.) Damit wird x zu einer Variablen vom Typ number - allerdings ohne einen festen Wert. Der in x gespeicherte Funktionsaufruf wird bei jeder Verwendung der Variablen x ausgeführt. In unserem Beispiel ist die Funktion random 10 20; nicht deterministisch. Sie hat einen undefinierten Rückgabewert. Deshalb sind die Ausgaben von log x; in der Regel voneinander verschieden.

Die Auswertung der Funktion kann durch das Kommando

var evaluate;

erzwungen werden. Ist var eine Variable, der ein Funktionsaufruf zugewiesen wurde, oder ist var ein berechneter Wert, in dem mindestens ein unaufgelöster Funktionsaufruf steckt, wird die Berechnung jetzt durchgeführt.

Die Stärke dieses Konzeptes zeigt sich besonders beim Einsatz in einer Prozesssteuerung. Hier ist charakteristisch, das Variable von den Zuständen äußerer Parameter abhängen und der aktuelle Wert sich mit der Zeit ändert. Trotzdem kann mit diesen noch unbekannten Werten "normal" gerechnet und das Ergebnis zu dem Zeitpunkt wenn es benötigt wird, konkretisiert werden. Dass soll nachfolgend an einem Beispiel erklärt werden:

Angenommen es gibt im System ein Device mit dem Namen LightSensor. Das Reading state dieses Sensors hat einen Wert zwischen 0 und 255, der mit der Umgebungshelligkeit des Sensors korreliert. 0 steht für absolut dunkel und 255 für helles Sonnenlicht. Ziel soll es sein, eine Device lamp einzuschalten wenn der Wert kleiner als 40 und ansonsten auszuschalten.

Diese Funktionalität soll in einer Funktion gekapselt werden, da sie für eine Vielzahl von Kombinationen von Helligkeitssensoren, Lampen und Schwellwerten benötigt wird.

Die zu definierende Funktion soll mit einem Kommando der Gestalt

lamp on if brightness < threshold;

aufgerufen werden können. Dabei sind lamp, brightness und threshold die Parameter des Aufrufs. Die Definition einer solchen Funktion wäre z.B. (siehe selbstdefinierte Funktionen):

'(fhemDevice'lamp on if number'brightness < number'threshold) {
   repeat {
      state := if (brightness < threshold) "on" "off";
      set lamp state;
      wait brightness;
   };
};

Die erste Zeile enthält die Signatur aus den Symbolen on, if und < sowie den Parametern lamp von Typ fhemDevice und brightnes und threshold vom Typ number. Mit dem Symbol repeat wird eine Wiederholschleife gestartet. In dem Wiederholblock stehen zwei Kommandos. Das Erste state := if (brightness < threshold) "on" "off"; berechnet aus den Parameter brightness und threshold den gewünschten Schaltzustand der Lampe - "on" oder off. Dieser Zustand ist aber, wenn brightness oder threshold keine festen Werte darstellen, sondern Funktionsaufrufe sind, ebenfalls unbestimmt. Erst mit dem nächsten Kommando set lamp state;, mit dem die Hardware angesprochen wird, werden die hinterlegten Funktionen aufgerufen, die Berechnungen durchgeführt und das Ergebnis an das Device übergeben. Daran schließt sich ein wait-Kommando an, mit dem auf eine Veränderung des Parameters brightness gewartet wird. Steckt hinter brightness kein Funktionsaufruf sondern ein fester Wert ist der Wartebefehl natürlich wirkungslos. (Ein übergebener konstanter Wert ändert sich ja nicht.)

5.5 Gültigkeitsbereich

Stand: 21.11.19 16:21

Auf eine Variable kann in dem Block, in dem sie definiert wurde, und in allen untergeordneten Kommandoblöcken zugegriffen werden. Überall wo der Name der Variablen in Ausdrücken auftaucht, wird er durch den Wert der Variablen ersetzt.

Nach der Abarbeitung der Anweisungen:

a := 3;
b := 5;
c := a * (8 - b);

hat die Variable c den Wert 9.

Es gibt Situationen in denen dieses Verhalten nicht gewünscht ist. Dann soll das Symbol als ein solches in einem Kommando stehen bleiben. Das ist z. B. der Fall, wenn ich in einem untergeordneten Kommandoblock einer Variablen lokal einen anderen Wert zuweisen (auch eines anderen Typs) zuweisen möchte:

a := 3;
...
{
   a := "Hi!";
   ...
}

Die Zeile a := "Hi!"; ließt sich wie folgt: Weise der Variablen a, die jetzt den Wert 3 hat den Wert "Hi!" zu. Das steht zum einen im Widerspruch zur statischen Typisierung (und führt zu einer Fehlermeldung) und zum anderen hätte die Variable a nach dem Verlassen des inneren Blockes einen geänderten Wert. Der Weg um das zu Verhindern ist, ein Apostroph vor das Symbol zu setzen:

a := 3;
...
{
   'a := "Hi!";
   ...
}

Ein vorangestelltes Apostroph verhindert, das 'a als Variable a interpretiert wird, sondern es bleibt das Symbol a. Dann ließt sie die Zeile als: Definiere eine Variable a mit den Wert "Hi!". Diese Variable "verschwindet" mit dem Verlassen des inneren Blocks. Danach hat die Variablen a, wieder den Wert 3.

Um einen Block zu kapseln, d.h. zu Verhindern, dass in einem Block auf Variable verfügbar sind, die in übergeordneten Blöcken definiert wurden, dient das Kommando: encapsulate {... block ...}; Auf in übergeordneten Blöcken definierte Klassen, Funktionen und Devices kann trotzdem zurückgegriffen werden. Soll auf eine endliche Auswahl von Variablen in {... block ...} aus dem übergeordneten Block zugegriffen werden können, müssen diese nach dem Symbol encapsulate notiert werden:

a := 3;
b := "Hallo Welt!";
...
encapsulate a {
   a := 3 * a;
   b := a * a;
   log b;
81
   ...
};
log b;
Hallo Welt!

Die Variable a wird nach "unten" durchgereicht und die Variable b im gekapselten Block ist unabhängig von der Variablen b in übergeordneten Block.

6 Kommandos und Funktionen

Stand: 08.06.19 21:22

Er gibt keinen prinzipiellen Unterschied zwischen Funktionen die in λscript vordefiniert sind und selbst definierten Funktionen. Vordefinierte Funktionen können durch Eigene ersetzt oder sie können modifiziert werden. Selbst definierte Funktion fügen sich harmonisch in die Sprache ein. Das wichtigste Prinzip aber ist: Funktionen haben Inputs und Outputs, aber keine Nebenwirkungen! Sie bearbeiten keine Daten, die ihnen nicht explizit übergeben wurden. Die Kommandos aus denen eine Funktion besteht, bilden zwar einen Block, aber es ist nicht möglich auf Variable die außerhalb des Block definiert wurden zuzugreifen. (Ein Ausnahme bilden Devices.) Sehr wohl können aber in einer Funktion alle in übergeordneten Blöcken definierten Klassen und Funktionen verwendet werden.

6.1 Signatur eines Kommandos

Stand: 08.11.19 09:23

Funktionen werden durch Kommandos aufgerufen. Jedes Kommando ist eine Folge von Werten und Symbolen. Werte sind Instanzen von Klassen und damit kann jedem Kommando eindeutig eine Folge von Klassen und Symbolen zugeordnet werden. Diese Folge nennt man die Signatur eines Kommandos. Ein Beispiel:

(12 + 18)

Diese Kommando hat die Signatur 'number + number'. Stößt λscript auf ein Kommando, sucht es im aktuellen Block, ob für diese Signatur eine Funktion gefunden wird. Wird keine gefunden, wird im übergeordneten usw. gesucht. Wenn eine Funktion gefunden wird, dann wird sie mit den aktuellen Werten, hier 12 und 18, aufgerufen. Wird keine Funktion gefunden, beendet der Compiler seine Arbeit mit einer Fehlermeldung. In diesem Fall aber, wird das Ergebnis 30 zurückgegeben.

6.2 Definition eigener Kommandos

Stand: 08.11.19 09:29

Eine neue Funktion wird in dem Block definiert, in dem sie gelten soll. (Ihr Gültigkeitsbereich umfasst auch alle untergeordneten Blöcke.) Dazu wird zum einen die sie beschreibende Signatur, und zum anderen der Code der sie am Ende ausmacht, in einem Definitionsblock notiert.

Ersteres, die Signatur, wird in runden Klammer eingefasst. Damit λscript diese Liste nicht als ein Kommando interpretiert, wird ihr ein Apostroph ' vorangestellt. Wie, wenn einem Symbol ein Apostroph vorangestellt wird, verhindert es die Abarbeitung und die Liste bleibt unverändert. Die Klassennamen werden noch um den symbolischen Parameternamen, ebenfalls unter Benutzung von ' erweitert. Daran schließt sich der Definitionsblock mit den Kommandos an. Der Rückgabewert einer Funktion ist das Ergebnis der letzten Operation im Definitionsblock. Zur Verdeutlichung ein simples Beispiel:

Es soll eine Funktion geschrieben werden, die eine Zahle quadriert, also mit sich selbst multipliziert. Wenn a die zu quadrierende Zahl ist, soll der Funktionsaufruf a'qrt oder was identisch ist (vgl. Anmerkung um Zugriffsoperator) (a qrt) sein. Die Signatur ist also die Klasse number und das Symbol qrt. Das folgende Kommando definiert die Funktion:

'(number'x qrt) := {x x};

Die gequotete Liste enthält die Signatur: Die Klasse number mit dem symbolischen Parameter x gefolgt von dem Symbol qrt. Daran schließt sich der Deklarationsblock mit dem Kommando an. Weil das Multiplikationszeichen zwischen zwei Zahlen ist optional ist, wurde es hier weggelassen.

Diese Funktion kann nun im nachfolgenden Script eingesetzt werden. Man kann das Ergebnis anderen Variablen zuweisen oder in anderen Ausdrücken verwenden.

'(number'x qrt) := {x x};

log 25'qrt;
625
a := 3;
b := 4;
c := 5;
log (a'qrt + b'qrt - c'qrt);
0
log 2'qrt'qrt'qrt;
256

Diese Funktion kann auch in Weiteren neu definierten Funktionen verwendet werden. Nachfolgend wird eine Funktion isRrightTriangle definiert, die überprüft, ob die drei übergebenen Parameter die Seitenlängen eines rechtwinkligen Dreiecks sind. Der Rückgabewert ist vom Typ boolean.

'(number'x qrt) := {x x};

'(isRrightTriangle number'a number'b number'c) := {
   (a'qrt + b'qrt - c'qrt) = 0
};

log (isRrightTriangle 3 4 5) ;
true
log (isRrightTriangle 56 90 105);
false
log (isRrightTriangle 68 285 293);
true

Wird die Funktion '(number qrt) im Deklarationsblock der zweiten Funktion '(isRrightTriangle number number number) definiert, funktioniert sie nur dort und ist von außen nicht sichtbar.

'(isRrightTriangle number'a number'b number'c) := {
   '(number'x qrt) := {x x};
   (a'qrt + b'qrt - c'qrt) = 0
};

log (isRrightTriangle 3 4 5);
true
log (isRrightTriangle 56 90 105);
false
log (isRrightTriangle 68 285 293);
true

6.3 Vordefinierte Kommandos

6.3.1 Zweistellige Verknüpfungsoperationen

Stand: 26.11.19 18:37

Mit Objekten vom Typ number kann in der üblichen Weise gerechnet werden. Verwendet werden können die Grundrechenarten + - * / ^. Das Multiplikationszeichen kann, wie in der Mathematik üblich, auch weggelassen werden. Das Symbol ^ zwischen zwei Zahlen potenziert die erste Zahl. Die zweite Zahl, der Exponent, muss eine ganze Zahl sein. Es gelten die üblichen Vorrangregeln. Diese Reihenfolge kann mit Hilfe von Paaren runder Klammern ( ) verändert werden. Eine Division durch 0 oder der Versuch der Potenzierung mit einer gebrochenen Zahl führen zu einem definierten Programmabbruch. Beispiele:

a := 3 + 5 * 8;
a := 3 + 5 8;
Ergebnis ist in beiden Fällen 43
c := 3 ^ 2 + 4 ^ 2;
Ergebnis: 25
a := (3 + 5) * 8;
a := (3 + 5) 8;
Ergebnis ist in beiden Fällen 64

Es gibt eine Vielzahl von Operationen bei denen zwei Operanden mit Hilfe eines dazwischen stehenden Symbols miteinander verknüpft werden. Diese so genannten binären Operationen werden häufig, wie bei den oben erwähnten arithmetischen Operationen, einfach hintereinander geschrieben. Die Reihenfolge mit der die Operanden verknüpft werden, hängt von der Priorität ab, die dem Verknüpfungssymbol zugeordnet ist. Zuerst werden die Operanden verknüpft die das Symbol mit der höchsten Priorität verbindet. Folgen Symbole mit der gleichen Priorität aufeinander (nur durch einen Operanden getrennt) werden die Operationen von links nach rechts ausgeführt.

Die nachfolgende Tabelle verschafft einen Überblick über die möglichen Verknüpfungsoperationen. Sie ist nach der Priorität der definierenden Symbol geordnet. Das Symbol mit der niedrigsten Priorität steht ganz oben. In den Spalten OP 1 und OP 2 sind die Klasse der der Operand angehören muss notiert. Dort kann auch einen von diesen Klasse abgeleitete Klasse stehen. (Wo string steht, könnte auch word stehen, und wo number steht könnte auch measurement stehen, da diese Klassen von jenen abgeleitet sind. Der Eintrag value bedeutet, da alle Werte Abkömmlinge der Klasse value sind, dass hier ein beliebiger Wert stehen kann.

Symbol Op 1Op 2 Ergebnis Bemerkung
or
|
boolean oder- Verknüpfung. Der Ergebnisausdruck ist genau dann true, wenn einer der beiden Ausdrücke true ist.
and
&
boolean und - Verknüpfung. Der Ergebnisausdruck ist genau dann true, wenn beiden Ausdrücke true sind.
= value Test auf Gleichheit. Objekte sind gleich, wenn sie der selben Klasse angehören und in alle Merkmalen übereinstimmen. Gehören die Operanden unterschiedlichen Klassen an, beendet der Compiler das Programm mit einer Fehlermeldung.
!= value Test auf Ungleichheit. Objekte sind ungleich, wenn sie der selben Klasse angehören und in mindestens einem Merkmal nicht übereinstimmen. Gehören die Operanden unterschiedlichen Klassen an, beendet der Compiler das Programm mit einer Fehlermeldung.
<=
<
>
=>
<=>
<>
number, moment, string und alle Klassen, für die Operation <= definiert wurde. booleanVergleich von Objekten. (Siehe auch hier: Vergleichsoperationen) Strings werden lexikografisch verglichen. Der erste Zeitpunkt ist kleiner als der Zweite, wenn er vor diesem liegt.
~ string regExpr boolean Die Zeichenkette wird darauf getestet ob der Wert des regExpr auf sie passt.
!~ string regExpr boolean Die Zeichenkette wird darauf getestet ob der Wert des regExpr nicht auf sie passt.
+ number Zahlen werden addiert.
string Zeichenketten zusammengefügt. Das Symbol + ist optional.
timespan Die Zeitspannen werden addiert. Das Symbol + ist optional.
moment timespan moment Das Ergebnis ist der Zeitpunkt der gegenüber moment um timespan versetzt ist. Das Symbol + ist optional.
timespan moment moment
- number Ausführung der Subtraktion
moment timespan Berechnung der Zeitspanne zwischen zwei Zeitpunkten
moment timespan moment Berechnung eines Zeitpunktes der um den Wert von timespan vor moment liegt.
* number Multiplikation von Zahlen. Das Symbol * ist optional.
number string Vervielfachung eines Strings. number muss ganzzahlig und 0 oder größer sein.
/ number Division von Zahlen. Der Versuch einer Division durch 0 führt zu einem Programmabbruch.
^ number Potenzierung mit einem ganzzahligen Exponenten. Ist der zweite Operand nicht ganzzahlig, oder sind beide Operanden gleich 0 wird das Programm mit einer Fehlermeldung abgebrochen.

6.3.2 Operationen über bestimmte Klassen

6.3.2.1 boolean

Stand: 26.11.19 18:39

Hier werden einerseits die Kommandos aufgeführt, die logische Werte als Ergebnis haben. Andererseits die Kommandos, die logische Werte als Ausgangspunkt benutzen. Eine wichtige Gruppe von Funktionen mit einem boolschen Wert als Ergebnis sind Vergleichsoperationen.

6.3.2.1.1 Vergleichsoperationen

6.3.2.1.1.1 Test auf Gleichheit von Objekten

Stand: 20.11.19 08:15

Zum Vergleich von Objekten werden die Symbole = und != verwendet. Die Bedeutung der Symbole bei Zahlen liegt im Grunde auf der Hand. Das Ergebnis eines solchen Vergleiches ist immer von Typ boolean. Die Operationen für = und != sind für alle Klassen, auch für von Nutzer angelegte, definiert.

Zwei Objekte sind gleich, wenn sie der selben Klasse entstammen und alle Eigenschaften, die die Objekte ausmachen, paarweise gleich sind. In diesem Fall ist das Ergebnis des Vergleichs true. Stammen zwei Objekte aus unterschiedlichen Klassen oder unterscheiden sich in mindestens einem Merkmal sind sie ungleich, und das Ergebnis ist der Wert false.

6.3.2.1.1.2 Ordnen von Objekten

Stand: 27.11.19 13:38

Über den Objekten einiger Klassen ist die Operation <= definiert. Z.B. für Zahlen (number measurement), Zeichenktten(string word) und Zeitpunkten (moment). Hier ist die Bedeutung des Symbols <= unmittelbar einleuchtend. Diese Operation definiert eine gewisse Ordnung über die Objekte.

Es kann sehr hilfreich sein, eine solche Funktion auch über andere Klassen zu definieren.

Die Operation mit dem Symbol <= muss, in der Sprache der mathematischen Algebra gesprochen, eine "totale Quasiordnung" sein. Damit die Widerspruchsfreiheit dieser Operation gewährleistet ist, muss sicher gestellte sein, dass für alle möglichen Objekte x, y und z einer Klasse, folgende Bedingungen gelten:

Wurde die Operation <= definiert, existieren automatisch auch die Operationen mit den Symbolen <, => und > und müssen nicht explizit definiert werden. Diese sind dann wie folgt definiert:

Außerdem sind zwei weitere Operationen definiert. Dazu führt folgende Überlegung: Für zwei beliebige Werte x und y kann sowohl x <= y als auch y => x gelten. Das bedeutet nicht, dass x = y gilt - sondern sie sind nur bezüglich der definierten Ordnungsrelationelation <= gleich.

Dieser Fall lässt sich mit dem Kommando x <=> y testen. Der Rückgabewert dieses Ausdrucks ist genau dann wahr, wenn sowohl x <= y als auch y <= x zutreffen. D.h., x und y sind im Sinne der Ordnungsrelation gleich.
Trifft genau nur eine der beiden zu Aussagen, x <= y oder y <= x, zu, liefert das Kommando x <> y den Wert true. D.h., x und y sind im Sinne der Ordnungsrelation ungleich.

Für die Klassen number, string, moment, measurement, moment ist jeweils ein Kommando x <= y definiert. Diese Definitionen sind mit dem üblichen Verständniss des Zeichens <= in Übereinstimmung.

Die Definition einer solchen Ordnungsrelation <= ist nicht in jeder Klasse sinnvoll möglich. Z.B. bei dem Datentyp timespan. Denn, es lässt sich nämlich z.B. nicht allgemein sagen, ob ein Monat 1/0/0:00 mehr oder weniger als 30 Tage 0/30/0:00 lang ist. Deshalb führen die Kommandos:

a := 1/0/0:00;
b := 30/0:00;
v := a > b;

zu einer Fehlermeldung des Compilers und das λscript wird nicht gestartet.

Ist über einer Klasse (z.B. cls) eine Ordnungsrelation definiert, dann existieren neben den Operationen <, => , >, <=> und <> automatisch auch einige weitere Funktionen die auf der Existenz einer Ordnungsrelation beruhen:

a := min x1 x2 ... xn;
b := max x1 x2 ... xn;

Die erste Funktion liefert das kleinste Objekt der Werte x1 bis xn. Die zweite Funktion entsprechend das größte Objekt. Dabei müssen die Argumente sämtlich vom der Klasse cls abgeleitet worden sein.

Nachfolgend ein Beispiel für die Verwendung von selbst definierten Ordnungsrelationen:

Betrachtet werden soll die Menge der Schüler eine Schule. Jeder Schüler ist Repräsentant einer Klasse clsSchüler. Jeder Schüler hat zwei Merkmale: seinen Namen und die Klassenstufe. Der Name ist von Typ string und die Klassenstufe von Typ number. Der Standardwert ist hier 1. Die Definition der Klasse Schüler sieht wie folgt aus.

clsSchüler := value
   name string
   stufe 1
;

Die einzelnen Schüler (Max Meier in Klasse 1; Ina Schröder in Klasse 3; ...) werden nun wie folgt angelegt:

S1 := new clsSchüler name "Max Meier";
S2 := new clsSchüler name "Ina Schröder" stufe 3;
S3 := new clsSchüler name "Petra Krüger Dingelstedt";
...

Nun wird eine Vergleichsrelation definiert, die sich auf die Klassenstufe bezieht.

'(clsSchüler'x <= clsSchüler'y) := {x'stufe <= y'stufe};

Dabei wird auf die vordefinierte Vergleichsrelation bei Zahlen zurückgegriffen. Nun lassen sich unter Verwendung der zusätzlich zur Verfügung stehenden Kommandos einige Fragen beantworten:

# Sind Schüler 1 und Schüler 3 in der selben Klasse?
log (S1 <=> S3);
true
# Ist Schüler 2 in einer Klasse unter Schüler 1?
log (S2 < S1);
false
# Sind Schüler 1 und Schüler 2 in verschiedenen Klassen?
log (S1 <> S2);
true
...

6.3.2.1.2 Verknüpfung boolscher Werte

Stand: 26.11.19 18:46

Logische Werte können mit den Symbolen ! & | verknüpft bzw. verändert werden. Wie allgemein üblich führt das Symbol & zu einer und-Verknüpfung und | zu einer oder-Verknüpfung. Mit dem Zeichen ! ist die Negation verknüpft. Es kann dem zu negierenden boolschen Ausdruck sowohl vor- als auch nachgestellt sein.

a := true;
b := false;
log (a & b);
false
log (a | b);
true
log (! b);
true
log a'!;
false

6.3.2.1.3 Ablaufsteuerung durch boolsche Werte

Stand: 26.11.19 18:55

Kommandos mit dem Symbol if dienen z.B. dazu, in Abhängigkeit vom Wert eines boolschen Ausdrucks, einer Variablen den einen oder einen anderen Wert zuzuweisen, oder um das Script mit dem einen oder einem anderen Programmzweig fortzusetzen. Dafür gibt es mehrere Varianten.

Variante 1:

if boolean
   { Block1 }
   { Block2 }
;
weitere Kommandos

Hier wird in Abhängigkeit davon, ob der dem Symbol if folgende Wert true oder false ist, das Script mit dem Block1 oder dem Block2 fortgestzt. Danach werden, falls vorhanden, die weiteren Kommandos abgearbeitet. Fehlt der Block2 und ist der boolsche Wert false wird sofort mit den Kommandos nach dem if-Befehl fortgesetzt.

Variante 2:

if
   boolean1
      { Block1 }
   ...
   booleank
      { Blockk }
   { Block }
;
weitere Kommandos

Diese Variante ist eine Verallgemeinerung der Variante 1. Auch hier wird, wenn der boolsche Wert true ist, der unmittelbar folgende Kommandoblock ausgeführt und danach mit den weiteren Kommandos fortgesetzt. Ist keiner der boolschen Werte true wird der letzte Block ausgeführt - wenn er angegeben ist. Dieser Block ist optional. Fehlt er, und ist kein einziger boolscher Wert true, werden die weiteren Kommandos abgearbeitet.

Variante 3:

if wert
   wert1
      { Block1 }
   ...
   wertk
      { Blockk }
   { Block }
;
weitere Kommandos

Diese Variante arbeitet ähnlich wie Variante 2. Der Block1 wird ausgeführt wenn der Vergleich wert = wert1 zu true ausgewertet wird. Analog Block2 usw.

Ähnlich wie in den obigen Varianten der Programmablauf mit boolschen Werten gesteuert werden kann, ist auch eine gesteuerte Wertzuweisung möglich. Auch hier gibt es wieder mehrere Optionen.

Variante 1:

Objekt := if bool wert1 wert;

Hier wird in Abhängigkeit davon, ob bool den Wert true oder false hat, dem Objekt der Wert wert1 oder wert2 zugewiesen. Wichtig ist, das die Angabe des alternativen Wertes wert nicht optional ist. Diese Form ist ein Spezialfall der

Variante 2:

Objekt:= if bool1 wert1 ... booln wertn wert;

Hier wird, wenn ein boolscher Wert true ist, Objekt der nachfolgende Wert zugewiesen. Sind alle boolschen Werte false, bekommt Objekt den Wert wert. Auch hier ist die Angabe von wert zwingend erforderlich.

Variante 3:

Objekt := if test
   test1 wert1
   ...
   testn wertn
   wert
;

Auch hier wird, wenn test = test1 wahr ist, an Objekt der Wert wert1 übertragen, wenn test = test2 wahr ist, an Objekt der Wert wert2 übertragen usw. Sind alle Vergleiche falsch, bekommt Objekt den Wert wert. Auch hier ist die Angabe von wert zwingend.

6.3.2.2 number

6.3.2.2.1 Zählschleifen

Stand: 14.06.19 12:12

Mit dem Kommando:

forEach k -10 11 2 {
   Block
};

wird der Block mehrfach durchlaufen. Zuerst mit dem Wert -10 für die Variable k. Dann wird k um den Wert 2 erhöht. Der Block wird erneut durchlaufen usw. Der letzte Durchlauf findet mit dem Wert 10 statt, da der nächste Wert 12 größer als die obere Grenze ist. Die Schrittweite (hier 2) muss größer als 0 sein. Die Laufvariable (hier k) muss entweder als Variable vom Typ number angelegt worden sein, oder, wenn sie nicht existiert, wird sie an dieser Stelle angelegt. Steht k für den Wert einer anderen Klasse bricht der Compiler mit einer Fehlermeldung ab.

Nach dem Durchlauf der Schleife hat die Laufvariable den letzten Wert mit dem der Block durchlaufen wurde.

Wird die letzte Zahl (die Schrittweite) nicht angegeben wird dafür 1 genommen. Wird der Startwert nicht angegeben wird dafür ebenfalls 1 angenommen. Beispiele:

forEach i 5 {
   log i;
}
1
2
3
4
5
forEach i 5 9 {
   log i " zum Quadrat = " (i i);
}
5 zum Quadrat = 25
6 zum Quadrat = 36
7 zum Quadrat = 49
8 zum Quadrat = 64
9 zum Quadrat = 81

6.3.2.3 string, word

6.3.2.3.1 Zusammensetzen von Strings

Stand: 26.11.19 19:02

Das hier Gesagte gilt sowohl für Objekte von Typ word als auch von Typ string.

Zusammengefügt werden Zeichenketten in dem man die entsprechenden Objekte hintereinander schreibt. Beispiel:

x := "Hallo " "Welt" "!";

Die Verwendung des Symbols + als Verknüpfungsoperator ist optional möglich. Obiges Kommando ist dem Folgenden äquivalent:

x := "Hallo " + "Welt" + "!";

Danach hat x den Wert "Hallo Welt!". Logischerweise ergibt sich beim Zusammenfügen von Objekten des Typs word wieder ein Objekt des Typs word. Befindet sich in der Folge der zusammen zu fügenden Zeichenketten ein Objekt string ist das Ergebnis auch vom Typ string.

6.3.2.3.2 regEx mit Strings

Stand: 26.11.19 19:36

Beim Test, ob Zeichenketten eine bestimmte Struktur oder einen bestimmten Inhalt haben kann die regEx-Syntax benutzt werden. Als charakteristisches Zeichen wird hier die Tilde ~ benutzt. Beispiel:

string := "Hallo Welt!";
log (string ~ /welt/);
false
log (string ~ /welt/i);
true
log (string ~ /Welt/);
true

Für eine ausfühliche Dokumentation von regEx sei auch an dieser Stelle auf die umfangreiche Literatur verwiesen.

Auch für die direkte Bearbeitung von Zeichenketten wurde das Konzept von regEx implementiert - mit dem Präfix s. Beispiel:

string := "Hallo Welt!";
string ~ s/Welt/User/;
log string;
Hallo User!

Über die Arbeitsweise von regEx gibt es eine Vielzahl von Dokumentationen. Deshalb wird sie an dieser Stelle nicht vertieft.

6.3.2.4 moment, timespan

6.3.2.4.1 Operationen mit moment und timespan

Stand: 26.11.19 19:15

Für die Verknüpfung von Zeitpunkten und Zeitspannen sind eine Reihe von Funktionen vordefiniert. Für die Addition und Subtraktion dieser Objekte sei zunächst auf die Tabelle in Zweistellige Verknüpfungsoperationen verwiesen. Ergänzend soll an dieser Stelle auf ein paar Dinge hingewiesen werden:

Dazu ein paar Beispiele:

m := 31.1.2019;
log m;
31.1.2019 00:00,00
m := 31.1.2019 12:34,56;
log m;
31.1.2019 12:34,56
m := 31.1.2019 1/0/0
log m;
28.2.2019 00:00,00
m := 31.1.2019 1/0/12
log m;
28.2.2019 12:00,00

Aus einem Objekt vom Typ moment lassen sich durch die Symbole second, minute, hour, day, weekday, week, month, year die entsprechenden Bestandteile aus dem Wert des Objektes "herauslösen".

m := 31.1.2019 12:34,56;
log m'second;
56
log m'minute;
34
log m'hour;
12
log m'day;
31
log m'weekday;
4
log m'week;
5
log m'month;
1
log m'year;
2019

weekday liefert die Nummer des Wochentags zurück. (1 = Montag, 2 = Dienstag, ... , 7 = Sonntag)

Steht man vor der Aufgabe festzustellen, ob die aktuelle Uhrzeit in einem bestimmten Intervall, z.B. zwischen 8:00 Uhr und 12:30 Uhr, liegt, kann das folgende Kommando benutzt werden:

log [8:00 12:30];
true

Die beiden in den eckigen Klammern stehen Argumente sind von Typ timespan. Beide Werte dürfen keine Monatsangabe besitzen und die Länge des Zeitintervalls darf nicht kleiner als 0 und darf nicht größer als eine Tageslänge (24:00) sein. Das wird zur Laufzeit überprüft und das Script bei Nichterfüllung dieser Bedingungen mit einer Fehlermeldung beendet.

Das Ergebnis ist der boolsche Wert true, wenn die aktuelle Uhrzeit im angegebenen Intervall liegt. Ansonsten ist false der Rückgabewert.

Ist der zweite Wert in der eckigen Klammer kleiner als der Erste, wird das entgegengesetzte Ergebnis zurückgegeben. Dadurch werden "Mitternacht übergreifende" Auswertungen realisiert.

log [23:00 6:00];

Diese Abfrage liefert true zwischen Abends 11 Uhr und Morgens 6 Uhr und sonst false.

Steht man vor der Aufgabe festzustellen, ob die aktuelle Zeit vor oder nach einem definierten Zeitpunkt (z.B. heute 12:00 Uhr) liegt, lässt sich das so realisieren:

isVormittag := [0:00 12:00];
isNachmittag := [12:00 24.00];

Danach haben die Variablen isVormittag und isNachmittag den Wert true oder false je nachdem, ob die aktuelle Uhrzeit vor oder nach 12:00 Uhr ist.

6.3.2.4.2 Konvertierung von number nach timespan

Stand: 12.06.19 11:12

Für den Fall, das Werte von Typ number in Zeitspannen umgewandelt werden sollen, stehen einige Möglichkeiten bereit. Angenommen n ist eine Zahl, so lässt sich mit den folgenden Kommandos diese Zahl in eine entsprechende Anzahl von Sekunden, Minuten, Stunden, Tagen, Wochen, Monate und Jahre umrechnen:

n = 3;
log n'seconds;
,3
log n'minutes;
3,0
log n'hours;
3:0,0
log n'days;
3/0:0,0
log n'weeks;
21/0:0,0
log n'months;
3/0/0:0,0
log n'years;
36/0/0:0,0

Die Ausgangswerte für eine Umrechnung in Monate oder Jahre müssen ganze Zahlen sein. Anderenfalls endet λscript mit einer Fehlermeldung.

6.3.2.5 fhemDevices

Stand: 26.11.19 19:39

Es gibt eine Reihe von vordefinierten Kommandos um auf die in einer Installation enthaltenen Objekte vom Typ fhemDevices zuzugreifen. In der folgenden Tabelle steht

Kommando Bemerkung Beispiel
set fhemDevice Folge von Symbolen und/oder Werten Dieses Kommando führt einen set-Befehl an einem Fhem-Device aus. Dazu werden alle dem Device folgenden Parameter zu einem String (durch Leerzeichen getrennt) zusammengefasst. Daten werden mit Hilfe von toString in Strings umgewandelt. set Lamp on;
level := 50; set Rollo pct level;
[fhemDevice ReadingName] Dieses Kommando gibt den Wert des Readings mit dem Namen ReadingName als string zurück. s := [Thermostat humidity];
[fhemDevice ReadingName time] Dieses Kommando gibt den Zeitpunkt der letzten Änderung des Readings ReadingName als moment zurück. s := [Thermostat humidity time];
[fhemDevice] Dieses Kommando gibt den Wert des Readings state als string zurück. s := [Lampe];
[fhemDevice time] Dieses Kommando gibt den Zeitpunkt der letzten Änderung des Readings state zurück. s := [Lampe time];
[fhemDevice internal name] Dieses Kommando gibt aus dem Register Internals den Wert mit dem Namen name als string zurück. s := [Lampe internal TYPE];

6.3.2.5.1 fhemLambdaDevice

6.3.2.5.2 fhemHomematicDevice

Stand: 26.11.19 19:26
Kommando Bemerkung Beispiel
fhemDevice'channel_nn Dieses Kommando gibt das Fhem-Device zurück, das dem Channel mit der angegebenen Nummer entspricht. weather := [Thermostat channel 1];

6.3.2.6 forEach - Schleifen mit wechselnden Inhalten

6.3.2.6.1 Durchlaufen eines Stacks

Stand: 11.10.19 21:09

Ist myStack eine Variable vom Typ stack wird mit dem Kommando

forEach myStack v {
   Block
};

der Block für jedes Element (beginnend mit dem untersten Element) des Stacks abgearbeitet. Der Stack selbst wird dabei nicht verändert. Die Variable v muss entweder als Variable vom Typ der Elemente des Stacks angelegt worden sein, oder, wenn sie nicht existiert, wird sie an dieser Stelle angelegt. Steht v für den Wert einer anderen Klasse bricht der Compiler mit einer Fehlermeldung ab.

Nach dem Durchlauf der Schleife hat v den Wert des obersten Elements des Stapels.

6.3.2.7 Zufallsfunktionen

6.3.2.7.1 random - Zufallszahlen

Stand: 20.06.19 07:08

Zufallszahlen werden mit Kommandos die das Symbol random enthalten erzeugt. Dem Symbol können ein oder zwei ganze Zahlen folgen. Werden zwei Zahlen angegeben gibt die zugehörige Funktion ganzzahlige Zufallszahlen, die nicht kleiner als die Erste und nicht größer als die Zweite sind, zurück. Die zweite Parameter muss dann selbstverständlich größer als der Erste sein. Ist das nicht der Fall, wird das Script mit einer Fehlermeldung beendet.

log (random 0 6);
5
log (random 0 6);
1
log (random 0 6);
6
log (random 0 6);
0
log (random 0 6);
3

Folgt dem Symbol random nur eine Zahl, muss diese größer als 1 sein, und es werden Zufallszahlen zwischen 1 und diesem Parameter erzeugt.

6.3.2.7.2 shuffle - Mischen eines Stacks

Stand: 16.08.19 12:09

Das Kommando myNewStack := myStack shuffle; kopiert die Elemente des Stacks myStack in zufälliger Reihenfolge in den Stack myNewStack.

myStack := (new stack of word) << "a" "b" "c" "d";
log myStack;
(word|a,b,c,d)
log myStack'shuffle;
(word|c,d,a,b)
log myStack;
(word|a,b,c,d)

6.3.3 wait - Wartekommandos

Stand: 25.10.19 13:27

Wartekommandos dienen dazu, die Abarbeitung eines Scrips zu stoppen und an einem definierten Punkt fortzusetzen. Dieser Punkt kann sein:

Wurden mehrere Bedingungen angegeben, wird der Wartezustand beendet, wenn einer der obigen Punkte eingetreten ist.

Das charakteristische Symbol für die Programmierung von Wartezuständen ist wait. Es gibt mehrere, nachfolgend beschriebene, Varianten des wait-Kommandos.

6.3.3.1 wait - Basisform

Stand: 22.11.19 09:58

Die grundlegende Form des wait-Kommandos ist:

wait v1 v2 ... vn;

v1 bis vn sind Werte und stellen die Parameter des wait-Kommandos dar. Der Wartebefehl wird beendet wenn einer der Parameter "feuert". Ein Parameter feuert, wenn einer dieser Fälle eintritt:

Dazu gibt es im Abschnitt "threads" einige Beispiel. Ein Beispiel mit dem Bezug auf ein fhemDevice:

w := wait 1:00 [WZ_Thermostat];
if (w = 1) {
   command 11;
   ...
   command 1n;
} (w = 2) {
   command 21;
   ...
   command 2n;
};

In diesem Beispiel wird darauf gewartet, dass sich das Reading state des Devices WZ_Thermostat ändert. Außerdem wird der Wartezustand nach einer Stunde beendet. In diesem Fall ist der Rückgabert des wait-Kommandos 1 und es wird der erste Kommandoblock abgearbeitet. Ändert sich aber vor dem Ablauf der Stunde das Reading state, ist der Rückgabewert 2, und es wird der zweite Komamndoblock abgearbeitet.

6.3.3.2 wait - Blockform

Stand: 25.10.19 14:09

Von der obigen Variante ist diese Form des Aufrufs abgeleitet:

wait a1 ... an {blockA} b1 ... bn {blockB}   ...   v1 ... vn {block V};

Der Wartezustand wird hier beendet wenn einer der Parameter a1 bis vn feuert. Der letzte Block {blockV} ist optional. Feuert ein Werte von a1 bis an wird zunächst der Block {blockA} abgearbeitet und danach mit den Kommandos die dem wait-Befehl folgen fortgesetzt. Analog wird, wenn einer Werte b1 bis bn feuern, wird zunächst der Block {blockB} ausgeführt. Ist {blockV} nicht angegeben, und feuert einer der Parameter v1 bis vn wird unmittelbar mit dem Kommando nach dem wait-Befehl fortgesetzt.

Das Beispiel aus dem vorhergehenden Abschnitt lässt sich damit wie folgt schreiben:

wait 1:00 {;
   command 11;
   ...
   command 1n;
   } [WZ_Thermostat] {
   command 21;
   ...
   command 2n;
   }
;

6.3.4 threads - Parallel ablaufende Scripte

Stand: 08.06.19 21:26

Das charakteristische Symbol für den Start von parallel laufenden Prozessen ist thread.

Eine Möglichkeit des Aufrufs ist:

thread {
   ...
};

Das heißt, dem Schlüsselwort thread folgt ein Kommandoblock. Dieser wird gestartet und abgearbeitet. Ein Thread wird bis zu einem wait-Kommando ausgeführt. Danach wird gewartet bis ein anderer Thread bereit ist. In einem Thread kann auf alle in den übergeordneten Blöcken definierten Devices, Klassen und definierten Funktionen zugegriffen werden. Ansonsten ist der Kommandoblock des Threads gekapselt. Das bedeutet, der Zugriff auf die in den übergeordneten Blöcken verwendeten Variablen ist so nicht möglich. Sollen innerhalb des Threads Variable von außerhalb verwendet werden, müssen diese nach dem Schlüsselwort thread explizit aufgelistet werden.

a := "Abend!";
thread a {
   log "Guten " a;
};
log "Guten Morgen!";

Dieses Script gibt die beiden Nachrichten "Guten Morgen!" und "Guten Abend!" aus. Die Reihenfolge mit der das geschieht ist nicht definiert, da das System entscheidet, welcher der Threads wann abgearbeitet wird. Das ist im nachfolgenden Beipiel anders:

a := "Abend!";
w := 0;
thread a w {
   wait w;
   log "Guten " a;
};
log "Guten Morgen!";
w := 1;

Hier wird zuerst "Guten Morgen!" und danach "Guten Abend!" ausgegeben. Der Grund ist: Dem untergeordneten Thread wird eine zweite Variable w übergeben. wait w; stoppt den Thread bis sich w ändert. Diese Änderung durch das Kommado w := 1; erfolgt erst nach der Ausgabe von "Guten Morgen!".

Threads können ineinander verschachtelt werden. Das heist, in Threads können wieder Threads gestartet werden. Genau genomment ist es sogar so, dass jedes λscript einer fhem-Installation ein Thread eines systemweiten Scripts ist.

Mit dem Kommando terminate; wird ein laufender Thread beendet. Gleichzeig werden auch alle Threads beendet die in diesem gestartet wurden.

w := 0;
thread w {
   wait w;
   log "Guten Abend!";
   terminate;
   log "Gute Nacht!";
};
log "Guten Morgen!";
w := 1;

Hier wird "Gute Nacht!" nicht mehr ausgebeben, weil der Thread vorher beendet wurde.

Verwandt mit dem Kommando terminate; ist das Kommando stop;. Damit wird das gesamte λscript beendet.

6.3.5 repeat, always - Wiederholung von Programmteilen

6.3.5.1 repeat

Stand: 21.11.19 15:55

Kommandos mit dem Symbol repeat dienen dazu, Kommandoblöcke wiederholt zu durchlaufen. In seiner einfachsten Form, um einen Kommandoblock unendlich oft zu wiederholen, dient das Kommando:

repeat {
   command 1;
   ...
   command k;
};

Soll der Block n-mal (n ist eine ganze Zahl ≥ 0) wiederholt werden, wird die Anzahl nach dem Schlüsselwort notiert:

repeat n {
   command 1;
   ...
   command k;
};

Das Kommando quit; dient zum Beenden einer Schleife. Im nachfolgenden Beispiel wird stündlich protokolliert ob WZ_Lampe eingeschaltet ist. Die Schleife wird beendet, wenn das Device ausgeschaltet wurde:

repeat {
   if ([WZ_Lampe] ~ /off/) {quit};
   log "WZ_Lampe ist an";
   wait 1:0;
};

Das Kommando loop; startet sofort einen neuen Schleifendurchlauf. Dazu ein Beispiel:

repeat {
   wait 1:0;
   if ([WZ_Lampe] ~ /off/) {loop};
   log "WZ_Lampe ist an";
};

Auch hier wird stündlich protokolliert, ob WZ_Lampe eingeschaltet ist. Die Schleife wird nie beendet. Wenn das Device ausgeschaltet wird, wird wieder an den Anfang der Schleife, zum Wartebefehl, gesprungen.

Eine Variation diese Kommandos ist die Übergabe eines Zeitspanne. In diesen Fall wird die wiederholte Abarbeitung des Blocks erst nach der angegebenen Zeitspanne durchgeführt. Beispiel: Ein- bzw. Ausschalten einer Lampe nach 10 Minuten:

repeat 10,0 {
   set WZ_Lampe toggle;
};

Vor die Zeitspanne, kann eine Zeitpunkt (Typ moment) gesetzt werden. Liegt dieser Zeitpunkt in der Zukunft, wird mit der ersten Ausführen des Block gewartet bis der Zeitpunkt erreicht wird. Liegt der Zeitpunkt in der Vergangenheit, wird mit der erstmalige Ausführen des Block gewartet, bis durch wiederholte Addition der Zeitspanne ein zukünftiger Zeitpunkt erreicht wird.

repeat 22:00'today 10,0 {
   set WZ_Lampe toggle;
};

Auch hier kann als dritter Parameter eine ganze Zahl ≥ 0 angegeben werden. Dann wird die Schleife so oft durchlaufen wie diese Zahl angibt. Beispiel:

repeat hour 1:00 {
   repeat 0,1.5 now'hour {set WZ_Gong on}
};

Dieses Script lässt einen Gong zu jeder vollen Stunde schlagen. Die Anzahl der Schläge ist die Stundenzahl. Die äußere Schleife hat als Startzeipunkt hour. Das ist der Beginn der laufenden Stunde. Von dort an (der Zeitpunkt liegt in der Vergangenheit wird der Block repeat 0,1.5 now'hour {set WZ_Gong on} stündlich, zu jeder vollen Stunde, wiederholt. Dieser Block besteht aus einer Schleife die sofort startet und alle 1,5 Sekunden das Kommando zum Ertönen des Gongs gibt. Die aktuelle Stundenzahl liefert der Ausdruck now'hour (siehe hier). Diese ganze Zahl bestimmt, wie oft die Schleife durchlaufen wird - wie oft der Gong ertönt.

6.3.5.2 always

Stand: 20.06.19 10:29

Der in der Praxis häufig vorkommende Fall, das die Endloswiederholung eines Kommandoblocks in einem seperaten Thread gestartet werden soll, wobei die Variablen v1 bis vn an den Thread übergeben werden, würde so aussehen:

thread v1 ... vn {
   repeat {
      command 1;
      ...
      command k;
   };
};

Dafür gibt es die abkürzende Schreibweise unter Verwendung des Schlüsselwortes always:

always v1 ... vn {
   command 1;
   ...
   command k;
};

7 Auslagerung und Import von Scripten

Stand: 11.10.19 22:09

Um selbst definierte Klassen und Funktionen in mehreren Scripten verfügbar zum machen, kann man die entsprechenden Definitionen auslagern und mit dem Kommando import in jedem Script, an jeder beliebigen Stelle wieder einlesen.

Die Auslagerung kann in ein Device oder in eine externe Datei mit der Endung ".lbd" erfolgen.

Angenommen die eigenen systemweit benutzen Klassen und Funktionen stehen in der Datei "myLambda.lbd", dann können diese Definitionen in einem anderen Script mit dem Kommando:

import pfadangabe/mylambda.lbd;

zur Verfügung gestellt werden.

Es ist ebenfalls möglich, die eigenen Klassen und Funktionen in einem Device vom Typ lambda (z.B. mit dem Namen "myClasses") zu hinterlegen und mit dem Befehl:

import myClasses;

zu importieren.

8 Beispielprojekt Rollladensteuerung

8.1 Aufgabenstellung

Stand: 26.06.19 07:25

Nachfolgend soll ein λscript entwickelt werden, dass eine umfangreiche Steuerung der im Haus vorhandenen Rollläden ermöglicht. Zielstellung ist:

Das Script wird schrittweise entwickelt und kann leicht an die individuellen Anforderungen angepasst und erweitert werden.

8.2 Definition der notwendigen Klassen

Stand: 25.06.19 20:08

Die Steuerung der Rollläden soll raumbasiert erfolgen. In jedem Raum befinden ein oder mehrere Rollläden. Außerdem ist der Zustand des Raumes durch mehrere Parameter definiert. Diese sollen zunächst sein:

Dem entsprechend wird eine neue Klasse clsRoom definiert:

clsRoom := value
   name string
   indoorTemperature number
   heating boolean
   shadingTemperature 23
;

Die Eigenschaft shadingTemperature wird auf 23 °C vordefiniert. Die anderen Eigenschaften müssen bei der Erstellung von Objekten explizit angegeben werden. Als nächste Klasse wird eine Klasse clsShutter definiert. Sie wird von der Klasse fhemDevice abgeleitetet, und erweitert die in FHEM als Rollläden angelegten Devices um die notwendigen Eigenschaften. Diese sollen sein:

clsShutter := fhemDevice
   windowState word
   doorState word
   sunLeft 20
   sunRight 190
   shadingLevel 60
   upLevel 0
   downLevel 100
   autoLevel -1
   autoTime 1:00
;

Nachdem diese beiden grundlegenden Klassen angelegt wurden, werden die Definitionen so erweitert, das die Objekte, die sie repräsentieren sollen, miteinander verknüpft werden können. In die Definition von clsRoom wird ein stack mit den Rollos eingefügt. In die Definition von clsShutter wird eine Eigenschaft room hinzugefügt, die das Rollo in den Raum in dem er sich befindet bezeichnet.

clsRoom
   shutters (stack of clsShutter)
;
clsShutter
   room clsRoom
;

Danach werden Objekte definiert die die notwendigen Objekte für die Steuerung enthalten. Das sind:

myHouse := new stack of clsRoom;
mySun := new sun;

8.3 Definition der benötigten Objekte

Stand: 25.06.19 10:33

Als nächstes werden von den definierten Klassen die notwendigen Objekte abgeleitet und den realen Gegebenheiten spezifiziert. Zunächst werden die Räume angelegt. Als Beispiel soll hier ein Wohnzimmer definiert werden.

Wohnzimmer := new clsRoom
   name "Wohnzimmer"
   indoorTemperature '([WZ_Klima measured-temp] toNumber)
   heating '([WZ_Thermostat actuator] != "0")
   shutters (new stack of clsShutter)
;

An dieser Stelle ist von der Möglichkeit der funktionalen Programmierung in λscript gebrauch gemacht worden. (siehe auf hier.) Die Eigenschaft indoorTemperature ist ein Objekt von Typ number. (Die Raumtemperatur in °C) Dieser Variablen wird jetzt kein Wert zugewiesen, das macht auch keinen Sinn, da er sich permanent ändert, sondern ein Funktion mit dem Rückgabewert number - erkennbar am Apostroph vor der öffnenden Klammer. In diesem Fall wird das Reading measured-temp des Klima-Channels mit dem Namen "WZ_Klima" eines Thermostats von Typ "HM-CC-TC" ausgelesen und in eine Zahl umgewandelt. Obwohl '([WZ_Klima measured-temp] toNumber) keinen konkreten Wert hat, kann damit gerechnet, kann die Eigenschaft mit anderen Variablen verglichen werden, kann sie als Parameter in Funktionsaufrufen dienen. Sie ist von einer gewöhnlichen Variablen nicht zu unterscheiden. Der konkrete Aufruf der Funktion findet erst dann statt, wenn das Ergebniss für eine Kommunikation mit der Hardwareperipherie des Systems benötigt oder die Auflösung mit Hilfe des Symbols evaluate erzwungen wird. Die Eigenschaft heating ist analog über ein Funktional definiert. Die Funktion liefert true, wenn das Heizkörperventil geöffnet ist. Zur Festlegung der Eigenschaft shutters wird ein neuer, zunächst leerer Stack angelegt.

In analoger Weise werden die anderen Räume definiert. Z.B. die Küche:

Küche := new clsRoom
   name "Küche"
   indoorTemperature '(2 + ([Flur_Klima measured-temp] toNumber))
   heating false
   shutters (new stack of clsShutter)
;

In diesem Beispiel wurde davon ausgegangen, dass in der Küche kein eigenes Thermostat vorhanden ist. Aus Erfahrung ist bekannt, das es immer 2 K wärmer ist als im Flur - und durch das Kochen und Backen die Heizung nie an ist.

So werden nacheinander alle Räume, in denen die Rollläden gesteuert werden sollen, angelegt. Diese Räume werden dann in das Objekt myHouse (ein Stack vom Typ clsRoom) eingespeichert.

Wohnzimmer Küche ... > myHouse;

Jetzt folgt die Integration der Rollladen-Devices. Beispielhaft sollen im Wohnzimmer zwei Fenster-Rollläden (die Namen der entsprechenden Devices in der FHEM-Installation seien WZ_Rollo1 und WZ_Rollo2) vorhanden sein. In der Küche gibt es ein Tür-Rollo mit dem Namen K_Rollo.

Stand: 25.06.19 10:35

wird fortgesetzt ...