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:

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:

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

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.

PaperUI
PaperUI

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.

CometVisu
CometVisu

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 demo label="Main Menu"
{
	Frame {
		Group item=gFF label="First Floor" icon="firstfloor"
		Group item=gGF label="Ground Floor" icon="groundfloor"
	}
	Frame label="Weather" {
		Text item=Sun_Elevation
		Text item=Weather_Temperature valuecolor=[Weather_LastUpdate=="NULL"="lightgray",Weather_LastUpdate>90="lightgray",>25="orange",>15="green",>5="orange",<=5="blue"] {
			Frame {
				Text item=Weather_Temp_Max valuecolor=[>25="orange",>15="green",>5="orange",<=5="blue"]
				Text item=Weather_Temp_Min valuecolor=[>25="orange",>15="green",>5="orange",<=5="blue"]
			}
			Frame {
				Switch item=Weather_Chart_Period label="Chart Period" mappings=[0="Hour", 1="Day", 2="Week"]
				Chart item=Weather_Chart period=h refresh=600 visibility=[Weather_Chart_Period==0, Weather_Chart_Period=="NULL"]
				Chart item=Weather_Chart period=D refresh=3600 visibility=[Weather_Chart_Period==1]
				Chart item=Weather_Chart period=W refresh=3600 visibility=[Weather_Chart_Period==2]
			}
		}
	}
	Frame label="Demo" {
		Text item=CurrentDate
		Text label="Group Demo" icon="firstfloor" {
			Switch item=Lights mappings=[OFF="All Off"]
			Group item=Heating
			Group item=Windows
			Text item=Temperature
		}
		Text label="Widget Overview" icon="chart" {
			Frame label="Binary Widgets" {
				Switch item=DemoSwitch label="Toggle Switch"
				Switch item=DemoSwitch label="Button Switch" mappings=[ON="On"]
			}
			Frame label="Discrete Widgets" {
				Selection item=Scene_General label="Scene Selection" mappings=[0=off, 1=TV, 2=Dinner, 3=Reading]
				Switch item=Scene_General label="Scene" mappings=[1=TV, 2=Dinner, 3=Reading]
				Setpoint item=Temperature_Setpoint minValue=16 maxValue=28 step=0.5
			}
			Frame label="Percent-based Widgets" {
				Slider item=DimmedLight switchSupport
				Colorpicker item=RGBLight icon="slider"
				Switch item=DemoShutter
			}
			Frame label="Map/Location" {
				Mapview item=DemoLocation height=10
			}
		}
		Text label="Multimedia" icon="video" {
			Frame label="Radio Control" {
				Selection item=Radio_Station mappings=[0=off, 1=HR3, 2=SWR3, 3=FFH]
				Slider item=Volume
			}
			Frame label="Multimedia Widgets" {
				Image url="http://localhost:8080/icon/splash-ipad-h.png" label="openHAB" {
					Text label="http://www.openHAB.org" icon="icon"
				}
				Video url="http://demo.openhab.org/Hue.m4v"
				Webview url="http://heise-online.mobi/" height=8
			}
		}
	}
}

Sitemap-basierte UIs gibt es so einige:

ClassicUI
ClassicUI
Android
Android
EiOS
EiOS

greenT

BasicUI - the new kid in town
BasicUI - the new kid in town

Sitemaps zeigen in der Praxis eine Reihe konzeptioneller und struktureller Schwächen:

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:

Das ist eine nette Liste, die so umgesetzt werden kann:

Struktur

Ein UIMap-Definition ist eine (optional hierarchische) Struktur aus Widget-Definitionen:

{
    "uuid": "51e2d009-0c85-43cd-acd9-d083b5934160",
    "name": "foo",
    "widgettype": "Map",
    "description": "Test Map Foo",
    "children": [
        {
            "itemname": "yahooweather_weather_12834203",
            "widgettype": "Frame",
            "attributes": {
                "label": {
                    "fixed": "Weather taken from Yahoo"
                    }
            },
            "children": [
                {
                    "itemname": "yahooweather_weather_12834203_humidity",
                    "attributes": {
                        "state": {
                            "script": "var s = item.getState(); var f = function(it) { if( s < 50 ) return it.getName() + ' is too dry:' + s; }; f(item);",
                            "mappings": [
                                { "state": "0",     "value": "dry" },
                                { "state": "50",    "value": "rather wet" },
                                { "state": "100", 	"value": "wet" }
                            ],
                            "pattern": "humindity: %d %%"
                        }
                    }
                },
                {
                    "name": "servertime",
                    "widgettype": "Text",
                    "attributes": {
                        "time": {
                            "script": "new Date().toString();"
                        }
                    }
                },
                {
                    "name": "reflect attr",
                    "widgettype": "Text",
                    "attributes": {
                        "label": {
                            "script": "'fixed value is: ' + attr.fixed",
                            "fixed": "this is a fixed label"
                        }
                    }
                }
            ]
        },
        {
            "widgettype": "Group",
            "name": "Tesla",
            "description": "how's the battery?",
            "attributes": {
                "label":        { "fixed":      "Tesla" },
                "tesla_widget": { "fixed":      "Tesla_Widget" }
            },
            "children": [
                {
                    "itemname": "yahooweather_weather_12834203_humidity",
                    "widgettype": "Slider",
                    "attributes": {
                        "label":        { "fixed":      "Tesla Battery" },
                        "state":        { "pattern":    "%d %%" },
                        "tesla_widget": { "fixed":      "Tesla_Loading_State_Widget" },
                        "tesla_label":  { "pattern":    "loaded up to %d" }
                    }
                }
            ]
        }
    ]
}

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.

{"attributes": {
    "label":        { "fixed":      "Tesla Battery" },
    "state":        { "pattern":    "%d %%" },
    "tesla_widget": { "fixed":      "Tesla_Loading_State_Widget" },
    "tesla_label":  { "pattern":    "loaded up to %d" }
}}

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:

{"attributes": {
    "state": {
        "script": "var s = item.getState(); var f = function(it) { if( s < 50 ) return it.getName() + ' is too dry:' + s; }; f(item);",
        "mappings": [
            { "state": "0",     "value": "dry" },
            { "state": "50",    "value": "rather wet" },
            { "state": "100",   "value": "wet" }
        ],
        "pattern": "humindity: %d %%"
    }
}}

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.

{
  "autocreated": false,
  "uuid": "4a8149c3-f240-429f-ad31-a98e271666f6",
  "itemname": "yahooweather_weather_12834203_humidity",
  "link": "http://localhost:8080/rest/uimap/widget/4a8149c3-f240-429f-ad31-a98e271666f6",
  "widgettype": "Slider",
  "itemtype": "Number",
  "itemcategory": "Humidity",
  "itemlabel": "Luftfeuchtigkeit",
  "itemstate": "70 %",
  "attributes": {
    "label": {
      "value": "Tesla Battery",
      "fixed": "Tesla Battery"
    },
    "state": {
      "value": "70 %",
      "pattern": "%d %%"
    },
    "tesla_widget": {
      "value": "Tesla_Loading_State_Widget",
      "fixed": "Tesla_Loading_State_Widget"
    },
    "tesla_label": {
      "value": "loaded up to 70",
      "pattern": "loaded up to %d"
    }
  }
}

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 GroupItems 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 GroupItems 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:

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:

public class DefaultAttributes implements OoItemVisitor {

    public DefaultAttributes( Item i ) {
        OoItem.create(i).accept( this );
    }

    ...
    
    @Override public void visit(OoGroupItem ooi)            { label(ooi); }
    @Override public void visit(OoColorItem ooi)            { label(ooi); }
    @Override public void visit(OoContactItem ooi)          { label(ooi); state(ooi); }
    @Override public void visit(OoDateTimeItem ooi)         { label(ooi); state(ooi); }
    @Override public void visit(OoDimmerItem ooi)           { label(ooi); state(ooi); switchenabled(); }
    @Override public void visit(OoImageItem ooi)            { label(ooi); }
    @Override public void visit(OoLocationItem ooi)         { label(ooi); state(ooi); }
    @Override public void visit(OoNumberItem ooi)           { label(ooi); state(ooi); }
    @Override public void visit(OoPlayerItem ooi)           { label(ooi); player(); } 
    @Override public void visit(OoRollershutterItem ooi)    { label(ooi); state(ooi); }
    @Override public void visit(OoStringItem ooi)           { label(ooi); state(ooi); }
    @Override public void visit(OoSwitchItem ooi)           { label(ooi); state(ooi); }
}