Aliasse

Eine Besonderheit stellt der Namespace alias.0 dar. Hier können eigene Objekte angelegt werden, welche Informationen aus der Objektstruktur spiegeln. Das heißt: Die Werte in dieser Struktur liegen nicht in der Zustands-Datenbank, sondern werden immer aus der Quelle errechnet. Sie belegen also keinen weiteren Speicherplatz im System, sondern zeigen nur auf bestehende Datenpunkte bzw. dessen Werte.

Die Idee

Warum sollte man das nutzen? Die Idee ist, dass in diesem Bereich jedes „Gerät“ im System erneut angelegt wird.

Diese „Alias-Datenpunkte“ kann man dann in der Visualisierung und in seiner Logik / Automation verwenden. Sollte man nun ein Gerät austauschen, muss man nur den Alias anpassen - der Rest funktioniert weiter.

Hat zum Beispiel ein HomeMatic- oder ZigBee-Taster einen Defekt und muss ausgetauscht werden, würde dieser mit einer neuen Seriennummer/ID in der jeweiligen Objektstruktur des Adapters (z.B. zigbee.0) angelegt werden. Jetzt müsste man alle Stellen im System suchen und ändern, wo der alte Taster verwendet wurde. Hat man aber vorher einen Alias angelegt, muss nur eine einzelne Stelle angepasst werden.

Man setzt also nur die Verknüpfung neu. Das ist zwar beim Anlegen ein Schritt mehr, aber das zahlt sich in jedem Fall hinterher aus!

Konvertierungen

Nun kann ein Alias aber nicht nur einen Wert 1:1 spiegeln, sondern die Daten auch in beide Richtungen (lesend und schreibend) manipulieren / verändern. So können zum Beispiel Berechnungen durchgeführt werden.

Bemerkung

Viele einfache Konvertierungen könnte man auch mit eigenen Scripts im JavaScript-Adapter realisieren. Das Ergebnis wäre das gleiche. Allerdings „verteilt“ man dann die Logik über das ganze System und es ist schwieriger den Überblick zu behalten. Ein Alias ist immer zu bevorzugen, da so auf den ersten Blick zu erkennen ist, wo die Daten eigentlich her kommen. Außerdem läuft die Berechnung der Alias-Werte intern ab, und benötigt keine laufende Instanz eines Adapters!

Der Admin-Adapter bietet dafür im „Objekt bearbeiten“-Dialog für Objekte im alias.0 Namespace ein weites Tab an, wo auch Konvertierungen beim lesen und schreiben aktiviert werden können.

  • „lesen“ bedeutet in diesem Fall, dass der Wert von der konfigurierten Quelle (Objekt-ID) geholt wird

  • „schreiben“ bedeutet, dass der Wert in die konfigurierte ID zurückgeschrieben wird, wenn sich der Wert im Alias ändert

Am Ende hat man hier alle Möglichkeiten, welche JavaScript bietet. Dabei wird der Parameter val (und weitere) angeboten, welcher den Wert des verknüpften Datenpunktes enthält.

Angenommen es gibt einen Datenpunkt, welcher die aktuelle Windgeschwindigkeit in Meter pro Sekunde (m/s) enthält. Jetzt soll der Wert aber in km/h umgerechnet werden. Genau das kann mit einem eigenen Alias sehr einfach gelöst werden:

Lesen (m/s in km/h umrechnen)

val * 3.6

Schreiben (km/h in m/s umrechnen)

val / 3.6

Eine Schreib-Funktion ergibt in diesem Fall wenig Sinn, da der Wert wahrscheinlich nicht geschrieben werden kann. Die Quelle wird „readonly“ sein. Der Wind wird sich nicht für unsere Geschwindigkeitsänderung interessieren!

Bitte beachte, dass die Datentypen der Zustände korrekt sind. Hier wird der Datentyp der Quelle höchstwahrscheinlich number sein. Also sollte unser Alias ebenfalls vom Typ number sein, da das Ergebnis der Multiplikation ja wieder eine number ist.

Ein weiteres Beispiel wäre, Grad Fahrenheit in Grad Celsius umzurechnen:

(val - 32) * 5 / 9

und beim Schreiben könnte man dann Celsius wieder in Fahrenheit zurückrechnen:

val * 1.8 + 32

Wert aus einem JSON extrahieren

Angenommen ein Zustand (Typ string oder json) enthält folgenden Wert: {"myAttr": 12, "blabla": "text"}. Dann kann dieses JSON direkt im Alias geparsed und das Attribut ausgelesen werden:

JSON.parse(val).myAttr

Ternary Operator

Möchte man einen boolschen Wert in einen String umwandeln, kann dafür der „Ternary-Operator“ genutzt werden. Liefert z.B. ein Fensterkontakt true (boolean) wenn das Fenster geschlossen ist, kann dieser Wert wie folgt in einen String gewandelt werden. Hier ist der Datentyp der Quelle boolean und der Datentyp des Alias string:

val ? 'geschlossen' : 'offen'

Ist der Ausgangswert numerisch, können hier natürlich auch einen Vergleich anstellen. Falls vom lesenden Zustand der Wert kleiner als 15 ist, soll z.B. der Text „kalt“ im Alias stehen. Hier ist der Datentyp der Quelle number und der Datentyp des Alias string:

val < 15 ? 'kalt' : 'warm'

Datum konvertieren

Angenommen der Ausgangswert ist ein Unix-Timestamp (z.B. 1650997245840). Diesen kann man dann nach belieben mit DateTimeFormat umwandeln.

Möchte man beispielsweise nur das Datum des Timestamps formatieren, dann gibts es folgende Möglichkeiten:

new Date(val).toISOString() // 2023-04-13T07:20:28.191Z
new Intl.DateTimeFormat('de-DE').format(new Date(val)) // 13.4.2023
new Intl.DateTimeFormat('de-DE', { dateStyle: 'short' }).format(new Date(val)) // 13.04.23
new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium' }).format(new Date(val)) // 13.04.2023
new Intl.DateTimeFormat('de-DE', { dateStyle: 'long' }).format(new Date(val)) // 13. April 2023
new Intl.DateTimeFormat('de-DE', { dateStyle: 'full' }).format(new Date(val)) // Donnerstag, 13. April 2023

Um nur den Wochentag zu extrahieren, gibt es folgende Möglichkeiten:

new Intl.DateTimeFormat('de-DE', { weekday: 'narrow' }).format(new Date(val)) // D
new Intl.DateTimeFormat('de-DE', { weekday: 'short' }).format(new Date(val)) // Do
new Intl.DateTimeFormat('de-DE', { weekday: 'long' }).format(new Date(val)) // Donnerstag

Möchte man nicht das Datum, sondern nur die Uhrzeit formatieren, gibt es verschiedene Möglichkeiten:

new Intl.DateTimeFormat('de-DE', { timeStyle: 'short' }).format(new Date(val)) // 09:20
new Intl.DateTimeFormat('de-DE', { timeStyle: 'medium' }).format(new Date(val)) // 09:20:28
new Intl.DateTimeFormat('de-DE', { timeStyle: 'long' }).format(new Date(val)) // 09:20:28 MESZ
new Intl.DateTimeFormat('de-DE', { timeStyle: 'full' }).format(new Date(val)) // 09:20:28 Mitteleuropäische Sommerzeit

Wenn man z.B. nur die Stunde und Minute im Format HH:SS haben möchte, wäre das wie folgt möglich (verschiedene Schreibweisen, gleiches Ergebnis):

`${new Date(val).getHours()}:${new Date(val).getMinutes()}` // 09:20
new Date(val).getHours() + ':' + ${new Date(val).getMinutes() // 09:20
new Intl.DateTimeFormat('de-DE', { timeStyle: 'short' }).format(new Date(val)) // 09:20

Natürlich kann auch beliebig kombiniert werden:

new Intl.DateTimeFormat('de-DE', { dateStyle: 'full', timeStyle: 'full' }).format(new Date(val)) // Donnerstag, 13. April 2023 um 09:20:28 Mitteleuropäische Sommerzeit
new Intl.DateTimeFormat('de-DE', { dateStyle: 'full', timeStyle: 'long' }).format(new Date(val)) // Donnerstag, 13. April 2023 um 09:20:28 MESZ
new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium', timeStyle: 'medium' }).format(new Date(val)) // 13.04.2023, 09:20:28
new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(val)) // 13.04.2023, 09:20

Werte runden

Um einen numerischen Wert auf eine bestimmte Anzahl Nachkommastellen zu runden, eigenet sich .toFixed(x). Diese Funktion liefert allerdings einen String zurück! Das Ergebnis müsste also wieder in einen numerischen Wert konvertiert werden.

Auf eine Nachkommastelle runden (mehrere Möglichkeiten):

Number(val.toFixed(1))
Math.round(val * 10) / 10

Der Trick: Math.round rundet immer auf eine natürliche Zahl. Wenn man nur eine Nachkommastelle erhalten möchte, kann man z.B. 123.45 mit 10 multiplizieren (ergibt 1234.5). Dann wird gerundet (ergibt 1234) und danach wieder durch 10 geteilt (ergibt 123.4).

Sollte der Ausgangswert vom Typ string sein, muss dieser vorher in einen numerischen Wert konvertiert werden:

Number(parseFloat(val).toFixed(1))
Math.round(parseFloat(val) * 10) / 10

Regulärer Ausdruck

Angenommen ein Zustand (Typ String) enthält folgenden Wert: 123.45°C (also inklusive Einheit). Hier könnte man mit einem regulären Ausdruck alles außer Zahlen entfernen und den Wert in eine Gleitkommazahl umwandeln:

Number(val.replace(/[^\d.]/g, ''))

Das ginge auch deutlich einfacher, wenn einfach parseFloat verwendet wird. Mit dieser Funktion werden einfach alle „nicht-Zahlen“ automatisch entfernt:

parseFloat(val)

Genauso könnte der Wert dann noch gerundet werden:

Math.round(Number(val.replace(/[^\d.]/g, '')))

Wert in Liste enthalten

Möchte man nur wissen, ob der aktuelle Wert in einer definierten Liste von Werten enthalten ist, könnte man dafür ein Array nutzen. Hier ist der Datentyp der Quelle number und der Datentyp des Alias boolean:

[32, 45, 543, 23.2, 4209].includes(val)

Das gleiche klappt auch mit Strings. Hier ist der Datentyp der Quelle string und der Datentyp des Alias boolean:

['wert1', 'wert2', 'yyy', 'cooles beispiel'].includes(val)

Eigene Logik ausführen

Am Ende ist es ganz normales JavaScript. Also spricht auch (technisch) nichts dagegen, eine neue (anonyme) Funktion zu definieren, welche sofort ausgeführt wird. Das könnte so aussehen:

((v) => { return v; })(val)
(function(v) { return v; })(val)

Warum das Ganze? Jetzt könnte man eigene Variablen deklarieren und damit weiter arbeiten. Würde ich das empfehlen? Eher nicht - aber es ist möglich. Worauf zugegriffen werden kann? Das kann man einfach herausfinden:

Object.getOwnPropertyNames(this).join(', ')

Die interessantesten Eigenschaften sind wahrscheinlich parseFloat, parseInt, RegExp, Date, JSON, Math, Intl - also die Beispiele von weiter oben in diesem Abschnitt.

Einschränkungen

Wie an den Beispiele zu sehen ist, gibt es extrem viele Möglichkeiten, einen einzelnen Wert zu verändern / zu manipulieren. Aber es gibt auch ein paar Nachteile/Einschränkungen, was mit einem Alias nicht möglich ist:

Werte überspringen / ignorieren

Da der Wert des Alias nicht in der Zustands-Datenbank landet, sondern immer berechnet wird, können keine Werte „übersprungen“ oder ausgelassen werden. Sollte z.B. der js-controller (oder das ganze System) neugestartet werden, muss der Wert wieder aus dem aktuellen Wert der Quelle berechnet werden können.

Werte aus mehreren Datenpunkten berechnen

Quelle und Ziel eines Alias ist immer genau ein Datenpunkt! Man kann nicht die Werte von zwei oder mehr Datenpunkten zusammenführen und daraus neue Werte berechnen. Dafür ist dann doch wieder ein Script (wie zum Beispiel im JavaScript-Adapter) notwendig.