Ein öffentliches Smart Home

Live Daten anzeigen mit Next.js, Serverless Functions und homee

24. Januar 2021

7 min

In Zeiten von Corona und Kontaktbeschränkungen können es die kleinsten Dinge sein, die uns einander näher bringen. Ab sofort kann man hier auf meiner Website direkte "Einblicke" in unser Zuhause bekommen.

Ich habe ein öffentliches Smart Home Dashboard gebaut!

Smart Home Dashboard

Natürlich war mal wieder eher der Weg das Ziel – deshalb möchte ich genau diesen hier gerne vorstellen.

Was dabei herausgekommen ist, ist eine Widget-Ansicht, die einige Werte aus unserem Zuhause auf der Startseite von timoclasen.de anzeigt. Die Werte kommen live aus unserer Smart Home Zentrale homee.

Die größte Herausforderung war es also, das Ganze so aufzusetzen, dass:

  • Nur die gewünschten Werte aus unserem Smart Home bereitgestellt werden
  • Man durch die Integration keinen Zugriff auf unser Smart Home bekommen kann
  • Unser homee nicht durch zu viele Anfragen überlastet oder außer Gefecht gesetzt werden kann

Das System besteht aus mehreren Komponenten:

  • homee als Smart Home Zentrale
  • Einer Serverless Function als serverseitigem Proxy zwischen Website und homee
  • Der Website mit clientseitiger React App zur Visualisierung der Daten

Serverless Function als Proxy

Da es aus Sicherheitsgründen keine Option ist, die homee Zugangsdaten direkt im Client (der Website) zu hinterlegen, muss es eine Server-Komponente her. Hier haben wir den perfekten Anwendungsfall für eine Serverless Function.

Eine Serverless Function ist ein Stück Code, welcher gezielt aufgerufen werden kann, seien Zweck (die definierte Funktion) erfüllt und dann wieder ruht, bis er wieder aufgerufen wird.

Das React Framework Next.js, mit dem ich meine Website gebaut habe, bietet zum Erstellen von Serverless Functions eine sehr einfache Möglichkeit. Alle JavaScript Dateien, die man in /pages/api/ (API Routes) anlegt, werden automatisch zu Serverless Functions in denen das folgende Konstrukt zur Verfügung steht:

1export default function handler(req, res) {
2 // Serverless function…
3}
1export default function handler(req, res) {
2 // Serverless function…
3}

Um das Smart Home Dashboard mit Daten zu versorgen, wird in diesem Handler dann bei jedem Aufruf:

  1. Eine Verbindung zu homee hergestellt
  2. Alle Geräteinformationen abgerufen
  3. Aus den Geräteinformationen, die benötigten extrahiert und für das Smart Home Dashboard aufbereitet
  4. Die für das Smart Home Dashboard nötigen Informationen zurückgegeben

Durch diese Architektur kann sichergestellt werden, dass Websitebesucher:innen keinen direkten Zugang zu meinem homee Zuhause bekommen.

homee die Smart Home Zentrale

Als Smart Home Zentrale setze ich auf homee (bin da natürlich etwas voreingenommen).

homee Web App

homee ist eine modulare Smart Home Zentrale, mit der man Hersteller unabhängig (WLAN, Z-Wave, Zigbee, EnOcean, BiSecur und bald WMS) Smart Home Geräte steuern, verbinden und automatisieren kann. homee legt viel Wert auf Datenschutz, alle Smart Home Daten liegen nur auf meinem homee bei mir Zuhause vor. Es gibt keine Hersteller-Cloud. Aus diesem Grund ist es nötig, direkt die homee Hardware bei mir Zuhause anzusprechen und nicht über eine Cloud-to-Cloud Kommunikation gehen zu können (da nicht vorhanden).

homee besitzt eine auf WebSockets basierende Schnittstelle. Nach dem Öffnen der Verbindung werden die Smart Home Daten zwar eigentlich gestreamt, ich missbrauche die Schnittstelle hier aber einfach REST-artig (Öffnen, Abfragen, Schließen).

Ich verbinde in meinem homee eine Vielzahl von smarten Geräten. Für mein Smart Home Dashboard war die Kunst zu entscheiden, welche Werte man öffentlich auf seiner Website präsentieren kann, ohne dass die Privatsphäre eingeschränkt wird. Kritisch wäre es, wenn man über die Werte z.B. erkennen könnte, ob jemand zu Hause oder vor allem nicht zu Hause ist. Deshalb habe ich bei den Klima-Werten davon abgesehen, den CO2-Wert zu veröffentlichen.

Zum Start habe ich mich für folgende Werte entschieden:

  • Raumtemperatur (Wohnzimmer)
  • Luftfeuchtigkeit (Wohnzimmer)
  • Aktueller Energieverbrauch (Alle Geräte, die mit einer Stromzähler-Steckdose ausgestattet sind)
  • Lichter (Alle aus / mindestens ein Licht an)
  • Außentemperatur (auf dem Balkon)
  • Regensensor (am Balkon)

Caching im Server

Was ich natürlich vermeiden wollte ist, dass jeder Aufruf meiner Website eine Anfrage an meinen homee bedeutet. Zum einen wäre das sowieso unnötig, da so Anfragen mit den gleichen Antworten produziert werden, zum anderen stellt das aber auch eine Sicherheitslücke für meinen homee dar. Unter Umständen könnte dieser so durch zu viele Anfragen gezielt überlastet werden und ich würde ggfs. Probleme bekommen, wenn ich in diesem Moment selber auf eine wichtige Funktionalität in meinem Smart Home zugreifen müsste.

Hier kommt Caching ins Spiel. Ich habe festgelegt, dass es reicht, wenn die Daten im Smart Home Dashboard im schlimmsten Fall eine Genauigkeit von 10 - 30 Minuten haben – aber grundsätzlich möglichst aktuell (live) sein sollten.

In meiner Servelerss Function setze ich also folgenden Header, um das serverseitige Caching zu konfigurieren:

1res.setHeader(
2 'Cache-Control',
3 'public, s-maxage=600, stale-while-revalidate=1200'
4);
1res.setHeader(
2 'Cache-Control',
3 'public, s-maxage=600, stale-while-revalidate=1200'
4);

s-maxage=600 sagt dem Server, wie lange die ge-cachten Daten als aktuell gelten. In meinem Fall also für 10 Minuten. In dieser Zeit liefert die Serverless Function also immer ge-cachte Daten aus. Sind 10 Minuten seit dem letzten Aufruf vergangen, gelten die Daten als "stale" (abgelaufen).

stale-while-revalidate=1200 gibt dem Server danach eine Zeitspanne von 20 Minuten, in der Folgendes passiert: Wird die Seite (bzw. die API) in dieser Zeit aufgerufen, werden beim ersten Mal trotzdem die ge-cachten Daten ausgeliefert. Im Hintergrund wird der Cache aber mit neuen Daten aus dem homee aktualisiert. Der nächste Besucher bekommt dann direkt aus dem Cache die gerade aktualisierten Daten ausgeliefert.

Kommt der nächste Besucher erst wieder nach 30 Minuten (10 + 20), bekommt er direkt frische Daten aus meinem homee (muss dann aber für die Anzeige etwas länger warten).

Diese Caching-Stratgie soll dazu führen, dass möglichst viele Besucher:innen ge-cachten Daten bekommen und nicht auf den längeren Weg (Website -> Server -> homee) warten müssen.

Bei hochfrequentierten Websites funktioniert das natürlich viel besser, aber das ganze Experiment hier ist ja auch eher zur Übung da.

Daten abfragen und anzeigen

Auf der Client-Seite, also in der Website, rufe ich dann die oben beschriebene Samrt Home API (Serverless Function) auf.

Da es sich um Live-Daten handelt, ist es hier nicht möglich, die Daten beim Bauen der Website statisch zu generieren. Sie müssen zur Laufzeit live abgefragt werden.

Hier habe ich mich dazu entschieden, die Library SWR zu benutzen, die einen ganz dünnen Wrapper um die native fetch() Funktionalität darstellt.

1import { Thermometer } from 'react-feather';
2import useSWR from 'swr';
3
4import SmartHomeElement from '@/components/SmartHomeElement';
5
6function fetcher(...args) {
7 const res = await fetch(...args);
8 return res.json();
9}
10
11export default function SmartHomeDashboard() {
12 const { data, error } = useSWR('/api/smarthome', fetcher);
13
14 return (
15 <SmartHomeElement
16 Icon={Thermometer}
17 title="Raumtemperatur"
18 value={error ? 'Nicht erreichbar…' : data?.temperature}
19 />
20 )
21}
1import { Thermometer } from 'react-feather';
2import useSWR from 'swr';
3
4import SmartHomeElement from '@/components/SmartHomeElement';
5
6function fetcher(...args) {
7 const res = await fetch(...args);
8 return res.json();
9}
10
11export default function SmartHomeDashboard() {
12 const { data, error } = useSWR('/api/smarthome', fetcher);
13
14 return (
15 <SmartHomeElement
16 Icon={Thermometer}
17 title="Raumtemperatur"
18 value={error ? 'Nicht erreichbar…' : data?.temperature}
19 />
20 )
21}

Sie bringt folgende Funktionen / Vorteile:

  • Deduplizieren von zeitgleichen Requests in mehreren React Komponenten
  • Erneutes Abfragen der Daten mit Backoff-Delay bei Fehler
  • Aktualisieren der Daten wenn der Browser oder das Tab mit der Website wieder in den Focus kommt
  • Eine State Maschine für "Lädt…" / "Daten erhalten" / "Fehler!"

Loading Skeletons

Um das Dashboard direkt rendern zu können und keine Layout Shifts zu erzeugen zeige ich während die Smart Home Daten abgefragt werden animierte Loading Skeletons an.

Loading Skeletons

Die Library, die ich dafür einsetzte, heißt react-loading-skeletons. Sie hat den Vorteil, dass man seine Komponenten nicht zweimal bauen muss, sondern die Skeletons inline, basierend auf bestimmten Bedingungen (z.B. der Sate Maschine) anzeigen kann. Die Skeleton-Elemente nehmen dann automatisch die gleiche Größe wie der sonst dargestellte Text an.

1import Skeleton from "react-loading-skeleton";
2
3export default function SmartHomeValue({ value }) {
4 return <p>{value ? value : <Skeleton width={75} />}</p>;
5}
1import Skeleton from "react-loading-skeleton";
2
3export default function SmartHomeValue({ value }) {
4 return <p>{value ? value : <Skeleton width={75} />}</p>;
5}

Viele weitere Ideen

Ich hoffe, euch gefällt mein kleines Experiment. Nehmt es gerne auseinander und zeigt mir, falls es doch Sicherheitslücken gibt. Ihr findet den ganzen Code wie immer auf GitHub.

In dieser ersten Version (PoC) habe ich jetzt nur Daten direkt dargestellt. Ich denke, in einer nächsten Iteration versuche ich die jeweiligen Daten noch aufzubereiten und mit mehr Aussagekraft zu hinterlegen.

Außerdem würde ich gerne noch ein interaktives Element hinzufügen, vielleicht dürft ihr ja bald auch mit unserem Smart Home interagieren!

Kontakt

Wir kennen uns und hatten schon länger keinen Kontakt mehr?

Klingt beruflich interessant und wir sollten mal gemeinsam zu Steuerbot sprechen?

Schreib' mir doch gerne 'ne E-Mail, folge mir oder vernetze dich!