Eigenbau-Sprachassistent Teil 3: Temperatur-Sensoren und Wetteransage
Im letzten Teil dieser Serie haben wir Rhasspy mit Node-RED verknüpft um dem Sprachassistenten die erste Funktion beizubringen: das Ausgeben des aktuellen Datums und Uhrzeit. In diesem Teil geht es neben der Wetteransage über das Internet um die Integration selbstgebauter Temperatur-Wächter auf Basis eines ESP32 via MQTT.
Temperatur-Wächter im Eigenbau
Temperatursensoren sind recht günstig zu haben und auch Mikrocontroller (z. B. Arduino oder ESP32) sind mit unter 10 Euro durchaus erschwinglich. Mithilfe der Arduino-IDE lassen sich die jeweiligen Komponenten einfach programmieren, um beispielsweise die Raumtemperatur auszulesen und mittels WLAN zu übertragen. Hinsichtlich der Temperatur-Sensoren ist die Auswahl jedoch groß und es ist schwer den Überblick zu behalten - hier eine Aufstellung einiger üblicher Sensoren und deren Preise:
Sensor | Verbindung | Volt | Ampere | Temperatur | Genauigkeit | Üblicher Preis |
---|---|---|---|---|---|---|
DHT11 | GPIO | 3 - 5,5 | 0,5 - 2,5 mA | 0 - 50 C° | + / - 5% | ~2 EUR |
Aosong AM2302 / DHT22 | GPIO | 3,3 - 6 | 1 - 1,5 mA | -40 - +80 C° | + / - 2% | ~4 EUR |
Bosch Sensortec BME280 | I²C, SPI | 1,7 - 3,6 | 340 μA | -40 - +85 C° | + / - 3% | ~5 EUR |
Einen sehr detaillierten Vergleichsbericht gibt es hier.
Ich habe mich nach anfänglichen Tests mit dem DHT-11/22 für den Bosch BME280-Sensor entschieden. Dieser erfasst neben Temperatur und Luftfeuchtigkeit auch noch den Luftdruck und - viel wichtiger - ist dabei viel genauer als die DHT-Sensoren. Bei Tests lagen zwischen dem DHT-22 und dem BME280 teilweise mehrere Grad Unterschied. Preislich liegt ebenfalls kein nennenswerter Unterschied zwischen den Produkten; auch ist der Stromverbrauch während der Messung geringer.
Als Mikrocontroller setze ich auf einen ESP32 - hier ist das genaue Derivat eigentlich egal, da jeder ESP32 WLAN und I²C (Inter-Integrated-Circuit) unterstützt. Da ich die Mikrocontroller ohne Netzteil betreiben will, habe ich mich für einen WEMOS LOLIN32 Lite entschieden, da dieser einen LiPo-Anschluss (Lithium-Polimer) für Batterien hat. Einer der Vorteile des ESP32 besteht darin, dass er in einen Deep Sleep-Modus versetzt werden kann, in welchem er sehr wenig Strom verbraucht: ca. 10 bis 20μA gegenüber 150 bis 300mA (=150000 bis 300000μA) im herkömmlichen Modus. Der niedrige Verbrauch wird durch die Abschaltung zusätzlicher Komponenten, wie WLAN, Bluetooth und dem Hauptprozessor erreicht. In Kombination mit einem 3.7 Volt 4000 mAh-Akku sollte der Mikrocontroller im besten Fall mehrere Monate lang seine Arbeit verrichten.
Eine sehr detaillierte Erklärung findet sich auf dieser Webseite
Dem ESP32 und der Programmierung der Logik könnte man mehrere Artikel widmen - das ist aber nicht Fokus dieser Artikelserie und interessiert möglicherweise nicht jeden in dieser Tiefe. Ich habe daher ein vorgefertigtes Programm auf GitHub veröffentlicht, das im Wesentlichen die folgenden Dinge tut:
- Auslesen der Temperatur und Luftfeuchtigkeit via I²C-Bus
- Herstellen einer Verbindung zu einem WLAN-Netzwerk und MQTT-Broker
- Veröffentlichen der erfassten Werte
- Versetzen in Deep Sleep für 15 Minuten
Neben dem Code befindet sich auch eine Skizze des Aufbaus im Repository. Mithilfe der Arudino-IDE kann der Code auf ein kompatibles Board übertragen werden.
MQTT in a nutshell
Message Queuing Telemetry Transport (MQTT) ist ein offenes Protokoll für die Kommunikation von Maschine zu Maschine. Es ist leichtgewichtig und lässt sich so auch auf Mikrocontrollern und eingebetteten Systemen mit schwacher Leistung betreiben. Dabei toleriert das Protokoll auch hohe Latenzen und sehr langsame Übertragungsraten. In der Regel nutzt MQTT den TCP-/IP-Stack, kann jedoch auch alternative Stacks verwenden (z. B. UDP, Bluetooth). MQTT findet vor allem in IoT-Projekten aber auch in komplexen Anlagen (Sensorenkommunikation, Maschinensteuerung) Verwendung.
Dreh- und Angelpunkt in einem MQTT-Szenario ist der dazugehörige Broker. An diesem läuft die Kommunikation zusammen, er sendet/empfängt als Server die Nachrichten der angebundenen Clients. Clients können nicht ohne Broker untereinander kommunizieren. Nachrichten sind standardmäßig unverschlüsselt, eine Kommunikation via TLS ist jedoch ebenfalls möglich. Darüber hinaus bieten Broker wie Eclipse Mosquitto die Option der Authentifizierung via Benutzername und Passwort.
Inhalte werden innerhalb MQTT als Topics strukturiert und ausgetauscht. Diese können hierarisch angeordnet und mit entsprechenden Beschränkungen versehen werden - beispielsweise um nur bestimmte Benutzer schreiben/lesen zu lassen. Ein Beispiel:
In diesem Beispiel gibt es 6 Topics: temp (Temperatur) und humi (Luftfeuchtigkeit) pro Raum (bath, work, sleep). Jedem Raum ist ein Benutzer zugeordnet (sensor_bath, sensor_work, sensor_sleep), der lediglich auf das eigene Topic Zugriff (lesend und schreibend) hat. Der Administrator (admin
) hat auf alle Topics lesenden und schreibenden Zugriff.
Clients können diese Topics abonnieren (subscribe), um über Änderungen informiert zu werden oder selbst Inhalte schreiben (publish). In unserem Beispiel veröffentlichen Temperatur-Wächter Nachrichten und Node-RED abonniert diese, um sie an Rhasspy weiterleiten zu können:
Jede Nachricht verfügt auch zwingend über eine QoS-Definition - zur Auswahl stehen:
Methode | Beschreibung |
---|---|
at most once | Nachricht wird einmal gesendet (bei Netzproblemen kommt diese ggf. nicht bei anderen Clients an) |
at least once | Nachricht wird solange gesendet, bis der Empfang bestätigt wird (kann ggf. mehrfach ankommen) |
exactly once | Nachricht kommt exakt einmal an, auch bei Netzproblemen |
Mithilfe des retained Flags kann der Broker auch als Zustandsdatenbank dienen, indem er den letzten Wert einer eingegangenen Nachrichten für andere/neue Clients vorhält. Das macht beim Temperatur-Wächter beispielsweise Sinn, damit neue Clients nicht bis zur nächsten Nachricht (also 15 Minuten in diesem Fall) warten müssen, um die aktuelle Temperatur zu erfahren.
Installation und Konfiguration von Mosquitto
Ich habe mich entschieden, Mosquitto als Container zu betreiben. Raspbian bietet zwar auch ein natives Paket an, jedoch hatte ich hier nach einigen Tagen immer Verbindungsabbrüche, die ich nicht zuverlässig beheben konnte. Der Broker war im Fehlerfall zwar noch aktiv, übertrug aber keine Nachrichten mehr und auch im Fehlerprotokoll fanden sich keine sachdienlichen Hinweise. Es half nur ein Neustart - unschön. Darüber hinaus habe ich ohnehin schon Rhasspy und Node-RED als Container im Betrieb - also stört ein weiterer Container nicht. 🙂
Das Mosquitto-Projekt bietet einen aktiv gepflegten Container für verschiedene Architekturen, unter anderem ARM, an. Für diesen habe ich das folgende docker-compose
-Konfigurationsdatei erstellt:
1version: "3"
2
3services:
4 mosquitto:
5 container_name: mosquitto
6 image: eclipse-mosquitto:latest
7 ports:
8 - "1883:1883/tcp"
9 volumes:
10 - "./mosquitto.conf:/mosquitto/config/mosquitto.conf"
11 - "./conf.d:/mosquitto/config/conf.d"
12 - "/mosquitto/data"
13 - "/mosquitto/log"
14 restart: unless-stopped
Den gesamten Code dieses und der folgenden Artikel gibt es auch auf GitHub
Über den TCP-Port 1883 ist der Dienst aus dem Netzwerk erreichbar - die Konfiguration-, Daten- und Protokolldateien werden per Volume ausgelagert. Die Hauptkonfigurationsdatei (mosquitto.conf
) sieht wie folgt aus:
1persistence true
2persistence_location /mosquitto/data/
3
4log_dest file /mosquitto/log/mosquitto.log
5
6include_dir /mosquitto/config/conf.d
Mit den persistence
-Parametern werden retained-Nachrichten prinzipiell erlaubt und ein Speicherort für die Datenbank definiert. Mit log_dest
wird der Pfad zur Protokolldatei definiert. Die include_dir
-Direktive erlaubt das Auslagern von Konfigurationsanweisungen in weiteren Dateien unterhalb des conf.d
-Ordners.
In der Datei conf.d/acl.conf
definiere ich Pfade zur ACL (Access Control List) und Passwortdatei. Anonymer Zugang ohne Account wird prinzipiell ausgeschlossen:
1acl_file /mosquitto/config/conf.d/acl
2password_file /mosquitto/config/conf.d/pwfile
3allow_anonymous false
Die ACL definiert drei Benutzer:
1# admin
2user admin
3topic readwrite #
4topic readwrite $SYS/#
5
6# operator
7user operator
8topic read #
9topic read $SYS/#
10
11# sensor_bath
12user sensor_bath
13topic readwrite bath/temperature
14topic readwrite bath/humidity
admin
hat vollen Zugriff auf alle Topics, während operator
zunächst nur lesenden Zugriff hat. Das ist der Benutzer, den Node-RED benutzen wird - in späteren Teilen dieser Artikelserie wird er noch weiteren Zugriff erhalten, um beispielsweise Steckdosen fernzusteuern. Der Benutzer sensor_bath
erhält nur in zwei für ihn relevanten Topics schreibenden Zugriff: bath/temperature
und bath/humidity
.
Die Passwortdatei ist eine einfache Textdatei im folgenden Format:
1<benutzer>:<passwort>
Das Passwort ist im PBKDF2-Format spezifiert - die einfachste Möglichkeit ist die Verwendung des mosquitto_passwd
-Kommandos, welches Bestandteil des mosquitto
-Pakets ist. Der erste Benutzer wird wie folgt angelegt:
1$ mosquitto_passwd -c pwfile admin
Weitere Benutzer werden hingegen mit dem folgenden Kommando erstellt:
1$ mosquitto_passwd -b pwfile operator
2$ mosquitto_passwd -b pwfile sensor_bath
Die ACL- und Passwortdateien dürfen nur vom Besitzer gelesen und geschrieben werden - andernfalls verweigert der Broker den Dienst:
1$ chmod 0600 acl{,.conf} pwfile
Abschließend wird der Container wie folgt erstellt und gestartet:
1$ docker-compose up -d
Die Verdrahtung mit Node-RED und Rhasspy folgt später in diesem Artikel.
Wer nun schon eine Platine mit meinem Beispielcode versehen hat, sollte Nachrichten per MQTT lesen können:
1$ mosquitto_sub -h <node-red-ip-adresse> -u sensor_bath -P <passwort> -t bath/temperature
224.68
Wetter-API
Wetterdienste gibt es dutzende im Internet und auch viele bieten eine REST API an. Oftmals jedoch nur gegen Bezahlung und/oder Registrierung - womit wir wieder beim Thema Datenschutz/Profiling wären. Auf GitHub gibt es glücklicherweise eine Zusammenstellung einiger kostenloser Dienste. Schlussendlich habe ich mich für MetaWeather entschieden, da es genau das bietet, was ich brauche - auch, wenn es derzeit noch eine Beta ist.
Um das aktuelle Wetter auszulesen, muss zunächst die nächstgelegene Stadt/Station gefunden werden. Hier hilft wieder curl
:
1$ curl -O https://www.metaweather.com/api/location/search/?query=Berlin
2[{"title":"Berlin","location_type":"City","woeid":638242,"latt_long":"52.516071,13.376980"}]
Relevant ist hier der Wert der woeid
-Eigenschaft - er identifiziert die entsprechende Stadt.
Das Wetter wird dann wie folgt ausgelesen:
1$ curl -O https://www.metaweather.com/api/location/<woeid>/
woeid
ist durch die entsprechende Stadt zu ersetzen!
Die Antwort ist sehr umfangreich und besteht im Wesentlichen aus folgenden Feldern:
Feld | Beschreibung |
---|---|
title |
Name der Stadt |
latt_long |
Latitude/Longitude |
time |
Aktuelle Uhrzeit |
sun_rise / sun_set |
Sonnenaufgang/-Untergang |
consolidated_weather |
Wetter für den aktuellen und folgende Tage |
sources |
für die Datenerfassung verwendete Quellen |
Um das aktuelle Wetter auszulesen, ist der Pfad consolidated_weather[0]
der JSON-Antwort interessant, er enthält unter anderem die folgenden Informationen:
Feld | Beschreibung |
---|---|
weather_state_name , weather_state_abbr |
Name bzw. Abkürzung des Wetterzustands (windig, bewölkt,...) |
wind_direction_compass |
Richtung, aus der der Wind kommt |
min_temp , max_temp , the_temp |
kälteste, wärmste bzw. aktuelle Temperatur |
wind_speed |
Windgeschwindigkeit |
air_pressure |
Luftdruck |
humidity |
Luftfeuchtigkeit |
Für den nächsten Tag wird der Pfad consolidated_weather[1]
gewählt, der übernächste Tag ist über consolidated_weather[2]
einsehbar, etc. Die Anzahl der vorhersehbaren Tage variiert je nach Stadt.
Sentences und Flow
Damit Rhasspy die neuen Funktionen anbieten kann, sind entsprechende Sentences in der Web-Oberfläche zu definieren:
1[GetTemperatureInRoom]
2room_name = (schlafzimmer | arbeitszimmer | bad | wohnzimmer | küche)
3wie ist die temperatur (im | in | in der) <room_name>
4wie ist die feuchtigkeit (im | in | in der) <room_name>
5wie (warm | kalt) ist es (im | in | in der) <room_name>
6wie (feucht | trocken ) ist es (im | in | in der) <room_name>
7
8[GetTemperature]
9wie ist die temperatur
10außentemperatur
GetTemperature
bezieht sich hier auf die Wetteransage per Online-API, während GetTemperatureInRoom
die Daten per MQTT auslesen soll. Hier sind bereits weitere Sensoren pro Raum definiert, sie können über den Platzhalter room_name
angesprochen werden. Es ist also nicht notwendig pro Raum ein einzelnes Kommando zu definieren. Nach einem Klick auf Train übernimmt der Assistent die Änderungen.
In Node-RED ist nun der Handler zu überarbeiten.
Zunächst werden dem Commands-Switch per Doppelklick zwei neue Fälle für die zwei neuen Kommandos hinzugefügt:
GetTemperature
- Temperaturansage über das InternetGetTemperatureInRoom
- Temperatur pro Raum via Sensor
Anschließend wird ein http request-Node in die Arbeitsfläche gezogen und mit dem GetTemperature
-Zweig verbunden. Folgende Einstellungen werden übernommen:
- Method: GET
- URL: https://www.metaweather.com/api/location/
/ - Name: Weather API
woeid
ist durch die entsprechende Stadt zu ersetzen!
Anschließend wird der Ausgabe des Nodes mit einem function-Node verbunden. Mit einem Doppelklick wird der Name auf Set values gesetzt und folgender Quellcode hinterlegt:
1var weather = msg.payload.consolidated_weather[0];
2flow.set("temp_outside", Math.round(weather.the_temp));
3return msg;
Dieser Code bezieht die aktuelle Temperatur und speichert sie in der Variablen weather
, die dem Flow als lokale Umgebungsvariable temp_outside
zugänglich gemacht wird. Diese Variable ist nur innerhalb des aktuellen Flows (also dem Rhasspy-Handler) sichtbar. Wir greifen gleich im nächsten Schritt darauf zu, um dem Sprachassistenten eine aussprechbaren Satz zurückzusenden.
Hierzu wird ein template-Node mit dem Ausgang der Funktion verbunden. Der Doppelklick werden die folgenden Einstellungen definiert:
- Name: Intent response
- Format: Mustache template
- Output as: Parsed JSON
Das Format muss hier wieder zwingend auf Mustache template gesetzt werden, damit die Variable entsprechend ersetzt wird. Das eigentliche Template sieht wie folgt aus:
1{
2 "intent": {
3 "name": "GetTemperature",
4 "confidence": 0
5 },
6 "speech": {
7 "text": "Draußen ist es {{ flow.temp_outside }} Grad warm."
8 },
9 "wakeId": "",
10 "siteId": "default",
11 "time_sec": 0.010800838470458984
12}
Hier sind wieder die von Rhasspy benötigten Werte hinterlegt - speech.text
enthält einen vorformulierten Satz, in welchem die aktuelle Temperatur eingefügt wird. Mit einem Klick auf Done wird der Assistent geschlossen und der Ausgang des template-Nodes wird mit dem bereits vorhandenen http reponse-Node (Send answer) verbunden.
Der Flow sieht nun wie folgt aus:
Nach einem Klick auf Deploy werden die Änderungen umgesetzt - testen lässt sich die Funktionalität wieder mit curl
:
1$ curl -H "Content-Type: application/json" -X POST -d '{"intent": {"name": "GetTemperature"}}' http://<node-red-ip-adresse>:1880/intent
2{"intent":{"name":"GetTemperature","confidence":0},"speech":{"text":"Draußen ist es 18 Grad warm."},"wakeId":"","siteId":"default","time_sec":0.010800838470458984}
Tada! Das sieht gut aus. 🙂
Zeit für die zweite Funktion - das Auslesen der Sensoren.
Hierzu ziehen wir einen weiteren function-Node in den Flow und verbinden den verbleibenden Switch-Fall (GetTemperatureInRoom) mit dem Eingang. Der Doppelklick erhält dieser den Namen Set values und folgenden Code:
1// set room
2var room = msg.payload.tokens.pop();
3flow.set("room", room);
4
5// round values
6var temperature = global.get("temperature");
7var humidity = global.get("humidity");
8flow.set("temperature", Math.round(temperature[room]));
9flow.set("humidity", Math.round(humidity[room]));
10
11return msg;
Hier wird eine Variable mit dem Namen room
definiert, die den letzten Eintrag des tokens
-Arrays aus dem Payload enthält. Wir erinnern uns - hier zerlegt Rhasspy die einzelnen Wörter des Satzes, wobei das letzte Wort den Raum enthält. Der Raum wird hier wieder als Umgebungsvariable deklariert. Danach werden zwei Variablen - temperature
und humidity
- definiert. Diese sollen die Werte per MQTT erhalten. In einem weiteren Handler, den wir gleich noch bauen, werden die Werte per MQTT ausgelesen und als globale Umgebungsvariablen definiert. Das ist deswegen notwendig, weil der MQTT-Handler ein eigener Flow ist und wir so nicht ohne weiteres auf die Variablen zugreifen können. Anschließend runden wir die Werte da wir keine Nachkommastellen benötigen und speichern diese als lokale Umgebungsvariablen für den aktuellen Flow. Die Logik ließe sich auch im bereits vorhandenen Rhasspy-Handler hinterlegen, würde dann aber nicht gerade zur Übersichtlichkeit der Logik beitragen - daher der Umweg über einen weiteren Flow.
Anschließend ziehen wir ein template-Node neben das function-Node und verbinden den Ausgang und Eingang. Der Doppelklick definieren wir erneut folgende Einstellungen:
- Name: Intent response
- Format: Mustache template
- Output as: Parsed JSON
Das Template sieht wie folgt aus:
1{
2 "intent": {
3 "name": "GetTemperatureInRoom",
4 "confidence": 0
5 },
6 "speech": {
7 "text": "Raum {{ flow.room }} ist {{ flow.temperature }} Grad warm, die Feuchtigkeit liegt bei {{ flow.humidity }}%."
8 },
9 "wakeId": "",
10 "siteId": "default",
11 "time_sec": 0.010800838470458984
12}
Hier enthält speech.text
erneut einen vorformulierten Satz mit den entsprechenden Werten.
Zuletzt wird der Ausgang des Templates wieder mit dem http response-Node verbunden - unser Flow sieht nun wie folgt aus:
Mit einem Klick auf Deploy werden die Änderungen übernommen. Bevor die Funktionalität vollständig ist, wird nun noch ein MQTT-Handler erstellt - hierzu erstellen wir per Klick auf das Plus-Symbol oben rechts einen neuen Flow. Dieser erhält den Namen Room handler.
Als erstes ziehen wir einen mqtt in-Node in den Arbeitsbereich und klicken doppelt drauf. Neben Server folgen Klicks auf Add new mqtt-broker und das Bleistift-Symbol. Im folgenden Dialog werden die entsprechenden MQTT-Zugangsdaten hinterlegt:
- Name: freie Wahl
- Server: IP-Adresse des Hosts
- Topic: erstelltes Topic, z. B.
bath/temperature
- Security -> Username: erstellter Benutzer
- Security -> Password: dazugehöriges Passwort
Mit einem Klick auf Done werden die Änderungen gespeichert. Als nächstes wird ein change-Node daneben gezogen und der Eingang des neuen Nodes mit dem Ausgang das vorherigen Nodes verbunden. Die folgenden Einstellungen sind zu übernehmen:
- Name: Store bath temperature
- Set:
global.temperature["bath"]
- To:
msg.payload
Das bewirkt, dass sobald eine Nachricht im MQTT-Topic bath/temperature eingeht, der entsprechende Inhalt im globalen Array temperature[bath]
gespeichert wird. Die Schritte werden nun für das zweite Topic (bath_humidity
) wiederholt. Der Flow sieht nun wie folgt aus:
Sollen mehrere Räume entsprechend versorgt werden, kann man die Schritte einfach für weitere Räume wiederholen. Nach einem Klick auf Deploy sind die Änderungen übernommen und eingegangene Messwerte werden fortan von Node-RED zwischengespeichert. Nach einem Neustart des Temperaturwächters sollten eingegangene Werte in der Debug-Ansicht ersichtlich sein.
Nun sollten beide Handler entsprechend miteinander interagieren. Zeit für einen finalen Test - diesmal über die Rhasspy-Oberfläche oder direkt per Sprachkommando.
Klasse, das sieht gut aus! 🙂
Ausblick
In diesem Teil hat unser Sprachassistent gelernt das aktuelle Wetter über das Internet auszugeben und lokale Temperatursensoren via MQTT auszulesen. Mit der Ausgabe der aktuellen Temperatur nutzen wir jedoch nur einen Bruchteil der API-Möglichkeiten - hier könnten weitere Kommandos zusätzliche Funktionen bereitstellen (z. B. Wettervorhersage für den nächsten Tag).
Im nächsten Teil kommt das Entertainment mit einem Online-Radio und einer Witze-API nicht zu knapp.