Eigenbau-Sprachassistent Teil 3: Temperatur-Sensoren und Wetteransage

  Christian Stankowic   Lesezeit: 32 Minuten

Im dritten Teil der Eigenbau-Sprachassistent-Serie geht es neben Integration selbstgebauter Temperatur-Wächter um die Wetteransage über das Internet.

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 gross und es ist schwer den Überblick zu behalten – hier eine Aufstellung einiger üblicher Sensoren und deren Preise:

Sensor Verbindung Volt Ampere Temperatur Genauigkeit Preis
DHT11 GPIO 3 – 5,5 0,5 – 2,5 mA 0 – 50 C° + / – 5% ~2 EUR
Aosong AM2302 / DHT22 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 Nachricht 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 die folgende docker-compose-Konfigurationsdatei erstellt:

version: "3"

services:
  mosquitto:
    container_name: mosquitto
    image: eclipse-mosquitto:latest
    ports:
      - "1883:1883/tcp"
    volumes:
       - "./mosquitto.conf:/mosquitto/config/mosquitto.conf"
       - "./conf.d:/mosquitto/config/conf.d"
       - "/mosquitto/data"
       - "/mosquitto/log"
    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:

persistence true
persistence_location /mosquitto/data/

log_dest file /mosquitto/log/mosquitto.log

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

acl_file /mosquitto/config/conf.d/acl
password_file /mosquitto/config/conf.d/pwfile
allow_anonymous false

Die ACL definiert drei Benutzer:

# admin
user admin
topic readwrite #
topic readwrite $SYS/#

# operator
user operator
topic read #
topic read $SYS/#

# sensor_bath
user sensor_bath
topic readwrite bath/temperature
topic 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:

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

$ mosquitto_passwd -c pwfile admin

Weitere Benutzer werden hingegen mit dem folgenden Kommando erstellt:

$ mosquitto_passwd -b pwfile operator
$ 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:

$ chmod 0600 acl{,.conf} pwfile

Abschließend wird der Container wie folgt erstellt und gestartet:

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

$ mosquitto_sub -h <node-red-ip-adresse> -u sensor_bath -P <passwort> -t bath/temperature
24.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:

$ curl -O https://www.metaweather.com/api/location/search/?query=Berlin
[{"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:

$ 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 folgendeTage
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:

[GetTemperatureInRoom]
room_name = (schlafzimmer | arbeitszimmer | bad | wohnzimmer | küche)
wie ist die temperatur (im | in | in der) <room_name>
wie ist die feuchtigkeit (im | in | in der) <room_name>
wie (warm | kalt) ist es (im | in | in der) <room_name>
wie (feucht | trocken ) ist es (im | in | in der) <room_name>

[GetTemperature]
wie ist die temperatur
auß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 Internet
  • GetTemperatureInRoom – 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/<woeid>/
  • 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:

var weather = msg.payload.consolidated_weather[0];
flow.set("temp_outside", Math.round(weather.the_temp));
return 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:

{
  "intent": {
      "name": "GetTemperature",
      "confidence": 0
  },
  "speech": {
    "text": "Draußen ist es {{ flow.temp_outside }} Grad warm."
  },
  "wakeId": "",
  "siteId": "default",
  "time_sec": 0.010800838470458984
}

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:

$ curl -H "Content-Type: application/json" -X POST -d '{"intent": {"name": "GetTemperature"}}' http://<node-red-ip-adresse>:1880/intent
{"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:

// set room
var room = msg.payload.tokens.pop();
flow.set("room", room);

// round values
var temperature = global.get("temperature");
var humidity = global.get("humidity");
flow.set("temperature", Math.round(temperature[room]));
flow.set("humidity", Math.round(humidity[room]));

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

{
  "intent": {
      "name": "GetTemperatureInRoom",
      "confidence": 0
  },
  "speech": {
    "text": "Raum {{ flow.room }} ist {{ flow.temperature }} Grad warm, die Feuchtigkeit liegt bei {{ flow.humidity }}%."
  },
  "wakeId": "",
  "siteId": "default",
  "time_sec": 0.010800838470458984
}

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: operator
  • 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.

Tags

MQTT, Temperatur, Node-RED, ESP32, Teil, Node, Flow, Rhasspy

Es wurden noch keine Kommentare verfasst, sei der erste!