Das Heimautomatisierungssystem Eclipse Smarthome (ESH) - meist in seiner Inkarnation als openHAB - ist wegen der Vielzahl sog. Bindings für die unterschiedlichsten IoT-Systeme auf dem Vormarsch. In mehr als 100 OSGi Bundles befasst sich ESH in seinem Kern jedoch nur mit drei Dingen:
Thing
Channel
Item
Things sind Dinge der echten Welt. Lampen, Schalter, Regler, Antriebe, Heizungen und die Kaffeemaschine. Diese Dinge haben Channels, über die sie ihren Zustand oder ihre Daten mitteilen oder Befehle entgegennehmen. Am anderen Ende eines Channels lauschen ein oder mehrere Items, die Abstraktion eines Bedienelementes. Items können aber auch einfach so existieren, man kann sie bedienen, es passiert nur nichts.
Den Bewohner mit seinem PC, Tablet oder Smartphone interessieren am Ende nur die Items, denn über sie interagiert er mit seinem wohligen Zuhause - wenn denn alles klappt.
Items gibt es in diversen Ausprägungen:
GroupItem
ColorItem
ContactItem
DateTimeItem
DimmerItem
ImageItem
LocationItem
NumberItem
PlayerItem
RollershutterItem
StringItem
SwitchItem
Im Grunde sind diese alle gleich, unterscheiden sich lediglich in Anzahl und Typ der Kommandos und Daten, die sie unterstützen.
Eine Sonderrolle spielt das GroupItem
, das eine Menge von Items aggregieren kann. GroupItems sind somit das Bastelelement zur Erstellung hierarchischer Item
-Strukturen: Wohnzimmer-Leseecke-Stehlampe. Oder zur Repräsentation eines Things mit mehreren Channels: Kaffeemaschinenkaffeebohnenbehälterfüllstand-Kaffeemaschinenwassertankfüllstand-Kaffeemaschinensonstwas. Oder zur beliebigen Gruppierung irgendwie zusammengehöriger Dinge: Alle Lampen im ersten Stock, alle Kinderzimmertürschlösser oder Rollläden im ganzen Haus.
Die Frage ist: Wie kommen abstrakte Bedienelemente auf einen Bildschirm? Dazu hat ESH mehrere Antworten:
- Reflektion der Item-Struktur
- Proprietäre Formate
- Sitemap
Reflektion der Item-Struktur
Eine naheliegende Möglichkeit, ein User Interface (UI) für ESH Items darzustellen ist, direkt auf die Items zuzugreifen.
PaperUI ist eine AngularJS SinglePageApp, die über das generische, REST-ähnliches Interface von ESH auf die Item-Struktur zugreift und sie reflektiert. Dabei wird eine definierte GroupItem
-Hierarchie und ein spezielles Item-Tagging (sog. ‚home-groups‘) vorausgesetzt. PaperUI kann seine Strukturen im Bedarfsfall auch erzeugen und dient so neben der reinen Hausarbeit auch zur Konfiguration.
Der Vorteil liegt auf der Hand: Es sind außer den Items keine weitere Infrastruktur- oder Datenformate notwendig, um das UI zu betreiben. Auf der Nachteil-Liste steht dem gegenüber, dass das UI mit den Informationen der Items auskommen muss. Spezielle Textmuster oder Farbzuweisungen können nicht hinterlegt werden.
Proprietäre Formate
CometVisu sieht chic aus, braucht aber eine proprietäre XML-Datei zu seiner Konfiguration. Wo die genau her kommt und wie sie ihren Weg zum Client findet, weiß ich gerade auch nicht.
Hier können UI-Elemente und Zusammenstellung unabhängig von der Item-Struktur definiert werden. Allerdings ist die Konfiguration nur für diese spezielle UI-Implementierung gültig. Für das Zurückschreiben von Definitionen existiert keine Schnittstelle.
Sitemap
Die meisten UI-Implementierungen bauen auf die sog. Sitemap, die als JSON-Dokument per REST vom Server geholt wird und das UI beschreibt. Eine Sitemap besteht aus Widget-Beschreibungen zu Items, die serverseitig mit dem Item-Zustand befüllt werden. Widgets gibt es in verschiedenen Typen und können ähnlich wie GroupItems gruppiert werden, ohne dass eine entsprechende GroupItem-Struktur existieren muss. Unter bestimmten Bedingungen können GroupItems für die Sitemap reflektiert und in Widget-Strukturen umgesetzt werden.
Hauptaufgabe der Sitemap ist es, den Status eines Items nach vorgegebenen Regeln in darstellungsfähige Information zu übersetzen (rendern). Teilweise bringen Items bereits eine Regel in Form eines String-Patterns mit, das auf den Zustand anzuwenden ist. Für die Sitemap können zusätzlich Regeln für Sichtbarkeit und Farbe angegeben werden. Das kann in etwa so aussehen:
Sitemap-basierte UIs gibt es so einige:
Sitemaps zeigen in der Praxis eine Reihe konzeptioneller und struktureller Schwächen:
- Erzeugt werden Sitemaps mit Hilfe einer anwendungsspezifischen Sprache (DSL), die vom Server interpretiert wird. Eine Änderung der Sitemap verlangt also direkten Zugriff auf das Dateisystem des Servers.
- Sitemaps sind nicht via REST speicherbar.
- es gibt allerdings einen Standalone-Editor auf Eclipse-Basis
- Das DSL-Format ist nicht viel les-/schreibarer, als das daraus erzeugte JSON, braucht aber viel Code und Abhängigkeiten.
- Updates des UI sind nur über Polling des gesamten Sitemap-Dokuments oder größeren Teilen davon (Pages) möglich. Zwar können per ServerSentEvents (SSE) Item-Updates vom Server abonniert werden und eine Zuordnung zu den Sitemap-Widgets ist möglich, aber das Rendern eines Widgets geschieht auf dem Server, so dass die Events auf dem Client nutzlos sind. Sie können lediglich zum Triggern eines Reloads dienen..
- Manche UIs wie z.B. greenT nutzen Events auf diese Weise serverseitig, in dem der Server einen Long-Poll für den Reload frei gibt.
- Updates per Reload führen gern zu einem Flickern oder Springen im Client.
- Keine Wiederverwendung von Widgetdefinitionen.
- Die Dokumentenstruktur ist nicht erweiterbar, so dass auch spezialisierte UIs mit den generischen Informationen auskommen müssen.
- Werden Widgets aus GroupItems erzeugt, gibt es stets nur eine Standardkonfiguration, die nicht editiert werden kann.
- Aufwändige Implementierung auf Basis des Eclipse Modeling Framework.
UIMap – eine mögliche Alternative
Nach Diskussion mit dem Sitemap-Autor und anderen ist UIMap enstanden. UIMap ist ein Vorschlag (Feedback erwünscht!) für eine Alternative zur Sitemap mit folgenden Zielen:
- RESTful
- erweiterbar
- spezialisierte UIs können ihre spezifischen Daten speichern
- Formen, Sounds, Layout, spezielle Widgettypen, Sortierung…
- wenig Client-Logik
- serverseitiges Rendern
- z.B. keine Auswertung von Sichtbarkeits- oder Farbregeln auf dem Client
- ein generischer Editor kann erstellt werden
- auch als WebApp
- Widgets einzeln speicherbar, nicht nur eine gesamte Struktur
- Wiederverwendung von Widgets
- innerhalb einer UIMap
- in unterschiedlichen UIMaps
- SSE Updates auf Widget-Basis
- verlangt, dass Widgets identifizierbar sind
- Beliebiges Rendering durch serverseitiges JS
- wegen Sicherheitsfragen eher optional
- Persistierung beliebiger, Item-bezogener Daten
- Suche Widgets nach Typ, Itemname oder Widgetname
- z.B. finde all Top-Level
Map
Widgets
- z.B. finde all Top-Level
- Ad-hoc UIMaps vom Client können gerendert und mit Updates versorgt werden, so dass ein Client spezifische UI-Daten vom Server verarbeiten lassen kann, ohne dass sie serverseitig persistiert sein müssen
Das ist eine nette Liste, die so umgesetzt werden kann:
Struktur
Ein UIMap-Definition ist eine (optional hierarchische) Struktur aus Widget-Definitionen:
Beim Speichern dieser Definition fügt der Server für jedes Widget eine UUID hinzu, falls sie nicht schon im Dokument definiert wird, die zur eindeutigen Referenzierung eines Widgets dient (z.B. in einem SSE Event, zum laden/speichern/löschen).
Besonderes Augenmerk verdient hierbei das attributes
Element.
label
und state
sind Standardattribute, die jedes Widget hat. In diesem Beispiel wurden für ein Tesla-spezialisiertes UI weitere Attribute hinzugefügt, die von einem generischen UI nicht ausgewertet werden. Hierin steckt die Erweiterbarkeit.
Attribute können selbst mit mehr oder weniger komplexen Renderregeln ausgestattet sein:
Im Beispiel wird script
serverseitig evaluiert. Liefert es kein Ergebnis, wird das mapping
versucht. Gibt es hier keinen Treffer, wird pattern
auf den Zustand des Item angewendet. Die Regeln aus der Sitemap gehören ebenfalls hierher, sind aber noch nicht implementiert.
Render
Wird diese Definition vom Server gerendert, werden die attribute
Definitionen auf das Item angewendet (value
), das durch itemname
referenziert wird. Außerdem werden die Item-Daten (itemname
, itemlabel
, itemcategory
, itemtype
, itemstate
) mit ausgegeben.
Generierung von UIMaps aus der Itemstruktur
Das autocreated
hat eine besondere Bedeutung: Es ist false
, wenn das Rendering aus einer Widget-Definition stammt. Bei Widgets, die ein GroupItem
referenzieren, wird für jedes Kind-Item, für das keine Widget-Definition vorgegeben ist, ein Default-Widget generiert. Für dieses wird autocreated
auf true
gesetzt und der Client hat die Freiheit, sie zu ignorieren. Er kann sie aber auch ggf. modifizieren und speichern. Auf diese Weise können Standard-Widget-Strukturen auch über die Modellierung mit GroupItem
s erzeugt werden.
ServerSentEvents
Da sich die Welt bewegt, ändert sich zuweilen auch der Zustand der Items. ESH verfügt von Haus aus über eine SSE-Schnittstelle, die über Änderungen an Items informiert. Da das Rendern der UIMap auf dem Server stattfindet, sind solche Events im Client nutzlos. Daher implementiert UIMap eine eigene SSE-Schnittstelle auf Widget-Basis. Sollte sich also der Zustand eines Item ändern, bekommt die UIMap das mit, sucht nach allen Widgets, die dieses Item referenzieren und sendet eine neu gerenderte Version an alle Clients, die sich für Events auf mindestens eines der Widgets oder eines in seiner Elternhierarchie registriert haben. Da alle Widgets über ihre UUID referenziert werden, kann der Client seine zugehörigen UI-Elemente finden und auffrischen. SSE Events können spezifisch für ausgesuchte Widgets und ihre Kinder empfangen werden. Damit kann die Netzlast an den dargestellten UI-Kontext angepasst werden.
REST
Als API für die UIMap ist eine REST-Schnittstelle vorgesehen, über die sowohl ein operatives UI als auch ein Editor UIMaps nutzen und bearbeiten können.
@POST uimap/render
Sende eine Widget-Definition an den Server und erhalte das gerenderte Ergebnis. Es wird eine UUID enthalten, mit der Events empfangen werden können (siehe @GET uimap/event/<uuid>
). Das Widget wird nicht gespeichert. Der Lebenszyklus des Widgets obliegt dem Client, er kann auch speichern oder löschen (@POST uimap/widget
,@DELETE uimap/widget/<uuid>
).
@GET uimap/find?widgettype=<type>&name=<name>&item=<itemname>
Liefert eine gefilterte Liste aller Widgets im System. Besonders nützlich, um Toplevel Map Widgets zu finden.
@GET uimap/widget/<uuid>
Rendert das Widget (rekursiv).
@DELETE uimap/widget/<uuid>?recursive=<true|false>
Löscht das Widget. Ist recursive=true
werden alle Kind-Widgets mitgelöscht. Das ist mit Vorsicht zu genießen, weil ein Kind-Widget auch an anderer Stelle verwendet sein könnte.
@POST uimap/widget
Speichere ein neues Widget. Befindet sich eine UUID im Dokument, darf sie noch nicht existieren. Fehlt die UUID, wird sie vom Server gesetzt.
@PUT uimap/widget
Speichere ein existierendes Widget. Die UUID im Dokument muss existieren. Alle Referenzen dieses Widgets erhalten den neuen Inhalt.
@GET uimap/createwidget/<itemname>
Erzeuge ein Default-Widget für ein gegebenes Item
. Das Widget ist vorerst nicht persistiert.
@GET uimap/createdefaultmap
Erzeuge ein Widget mit widgettype=Map
, das Kind-Widgets für alle GroupItem
s mit dem Tag home-group
enthält. Es wird vorerst nicht persistiert. Damit kann man sich mit einer beliebigen Item-Struktur verbinden, die z.B. von PaperUI erzeugt wurde.
@GET uimap/event/<uuid>
Registriere auf SSE Events für ein bestimmtes Widget und seine Kinder (rekursiv). Wird keine UUID angegeben, registriere auf die Events aller Widgets.
Schweizer Taschenmesser
Das Ziel der UIMap ist natürlich - wie der Name sagt - eine Definition von UI-Strukturen. Tatsächlich kann der Einsatzbereich deutlich weiter gefasst werden.
Beispiel: In einem Projekt war es notwendig, Items in einer sortierbaren Reihenfolge darzustellen. Das wurde mit Hilfe spezieller Tags an dem Items gelöst, die JSON-Dokumente enthielten. Ein ziemlicher Missbrauch der Tags und teilweise mit vielen, nicht-transaktionalen REST-Calls verbunden. Mit der UIMap wäre diese Aufgabe leicht mit einem speziellen Attribut sortindex
zu lösen gewesen.
Da UIMap Widgets keine Referenz zu Items benötigen und sicher per UUID referenzierbar sind, können sie auch für allgemeine Persistenzaufgaben herangezogen werden.
Fazit
Die UIMap ist implementiert und einsatzbereit, aber es gibt noch kein UI, das sie nutzt. Damit ist die Praktikabilität noch nicht erwiesen. Auch wäre ein Prototyp eines Editors wichtig, um die Designannahmen in der Praxis zu verifizieren und ggf. anzupassen.
Trägt das Design, sind weitere Features sinnvoll:
- Übersetzung der bestehende Sitemaps in UIMaps
- Deployment vordefinierter UIMaps (die nicht durch das REST-Interface gespeichert werden müssen)
P.S. dev sidekick
Etwas Kunsthandwerk bei der Implementierung: Sitemap verwendet endlose instanceof
Kaskaden, um auf verschiedene Widget-Typen zu reagieren. Für UIMap sollte das vermieden werden. Ein paar kanonische Wrapper-Klassen später gehen auch Visitors mit gefälligem Code statt instanceof
: