Eigene REST API mit Node-RED - Beispiel: Kurzbefehle Morgenroutine mit Siri

  • Node was?!

    Auf eurem System ist Node-RED noch nicht installiert? Kein Problem! Hier wird euch geholfen.

    Sollte ihr einen Raspberry Pi nutzen, empfehle ich euch die Verwendung des offiziellen Installation-Skript. So hab ich Node-RED installiert.


    Spielt ein bisschen mit Node-RED und lest euch im Internet das nötige Wissen an. Ich kann hier leider nicht bei Null anfangen, das würde absolut den Rahmen sprengen. Das hier gezeigte Beispiel richtet sich daher auch eher an erfahrenere Nutzer. Sorry! :saint:

    Wozu eine eigene REST API?

    Es gibt sicher viele Gründe, warum eine eigene REST API durchaus Sinn ergibt. Einer dieser Gründe ist beispielsweise, dass man Daten anderer APIs zusammenfassen und in Windeseile abfragen kann. Genau das will ich euch in diesem Beispiel einer Kurzbefehle Morgenroutine näher bringen.


    Es gab hier im Forum (und auch auf reddit) schon einige sehr gut Ansätze für Kurzbefehle, die nach dem Wecken ausgeführt werden. Dem Geweckten werden mit Siris lieblicher Stimme eine Menge Informationen um die (noch müden) Ohren hauen: Wetter, Verkehrslage, anstehende Termine im Kalender, Venustransit und Sonnenparallaxe. You name it!


    Das Problem ist oftmals, dass dabei entfernte Server angefragt werden, die nicht immer sofort und zeitweise auch überhaupt nicht reagieren. Unser Raspberry Pi hat diese Probleme natürlich nicht; er läuft zuverlässig wie ein Schweizer Uhrwerk. Jedenfalls meistens. 8o


    Node-RED bietet uns mit relativ einfachen Mitteln die Möglichkeit, unsere Kurzbefehle Morgenroutine mittels eigener REST API deutlich zuverlässiger ausführen zu lassen. Beispielhaft für die vielen Möglichkeiten wird in dieser Morgenroutine das aktuelle Wetter und die Verkehrslage auf dem Weg zur Arbeit abgefragt.

    Was wird benötigt?

    • Ein Raspberry Pi oder ein anderer Server, der dauerhaft im Betrieb ist
    • Eine laufende Installation von Node-RED
    • Ein iOS-Device auf dem die App Kurzbefehle installiert ist
    • Eine API-Key für GoogleMaps
    • Ein API-Key für OpenWeatherMaps
    • Etwas Erfahrung mit Node-RED und JavaScript ist von Vorteil

    Für dieses Beispiel werden keine zusätzlichen Module (nodes) verwendet. Alles was wir brauchen, liefert uns Node-RED bereits mit der Basisinstallation. Yay! 8)

    Bereit? Los geht's!

    Mit dem Pluszeichen fügen wir einen neuen Flow hinzu. Zunächst erstellen wir die Abfragen an GoogleMaps und OpenWeatherMaps. Da wir die APIs nicht unnötig belasten wollen, fragen wir die Werte per inject natürlich nur alle 10 Minuten und nur in einem bestimmten Zeitfenster (function und switch) ab. Nett von uns!


    Ab hier geht es zweigleisig weiter. Von den Servern wollen wir jeweils mittels http request ein analysiertes JSON-Objekt als Rückgabewert. Das bekommen wir auch. In den beiden darauf folgenden, untereinander liegenden function Nodes hübschen wir die erhaltenen Daten etwas auf. Per join geht es wieder eingleisig weiter, aber nur, wenn wir von beiden Servern eine Antwort erhalten haben.


    Im template Node erstellen wir die Textvorlage, die uns Siri von nun an jeden Morgen um die Ohren hauen soll. Dazu verwenden wir das mustache Format. Bei mir sieht das so aus.


    Code
    1. Die Fahrt zur Arbeit dauert aktuell {{payload.minutes_in_traffic}} Minuten. ... ...
    2. Draußen ist es {{payload.temp_text}}! ... Die Temperatur beträgt {{payload.temp}} Grad Celsius. ...
    3. Bei einer relativen Luftfeuchtigkeit von {{payload.humi}} Prozent ...
    4. und einer Windgeschwindigkeit von {{payload.wind}} KMH ...
    5. liegt die gefühlte Temperatur bei {{payload.feel}} Grad Celsius.


    Die Zeilenumbrüche dienen der Übersichtlichkeit, müssen aber vor dem Deploy entfernt werden. Die drei Punkte (...) zwingen Siri später, eine Pause beim Sprechen einzulegen. Als wäre sie ein Mensch und müsste kurz Luft holen. ;)


    Das so erstellte template machen wir mit set.flow in einem function Node für den gesamten flow verfügbar. Der erste Teil ist somit fertig:



    REST API Endpunkt

    Im http in Node geben wir die URL an, über die wir später unseren Text aufrufen wollen. Dem folgt dann ein function Node, der uns den zuvor über set.flow gespeicherten Wert im aktuellen payload zur Verfügung stellt. Wenn der Wert nicht Null ist (switch) wird aus dem payload ein brauchbares Format (json) generiert. Mittels change werden die nötigen Header (content-type:application/json) generiert und http response schickt schließlich die Antwort raus. Unser flow ist fertig! Weiter unten könnt ihr ihn euch kopieren.



    Code: flow
    1. [{"id":"ed332c3.038c7d","type":"tab","label":"Morgenroutine","disabled":false,"info":""},{"id":"b4c11fc0.0de828","type":"http in","z":"ed332c3.038c7d","name":"","url":"/rest/siri_morgens","method":"get","upload":false,"swaggerDoc":"","x":140,"y":100,"wires":[["7f905b0b.991ac4"]]},{"id":"dc59b198.4a1a08","type":"http response","z":"ed332c3.038c7d","name":"","statusCode":"","headers":{},"x":970,"y":100,"wires":[]},{"id":"872b278d.eb5e1","type":"json","z":"ed332c3.038c7d","name":"","property":"payload","action":"","pretty":false,"x":650,"y":100,"wires":[["3315174e.3ffe78"]]},{"id":"7f905b0b.991ac4","type":"function","z":"ed332c3.038c7d","name":"flow.get Siri","func":"var Siri = flow.get(\"Siri\")||0;\nmsg.payload = {\"Siri\": Siri};\nreturn msg;","outputs":1,"noerr":0,"x":350,"y":100,"wires":[["a437e365.2af878"]]},{"id":"3315174e.3ffe78","type":"change","z":"ed332c3.038c7d","name":"Set Headers","rules":[{"t":"set","p":"headers","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"headers.content-type","pt":"msg","to":"application/json","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":810,"y":100,"wires":[["dc59b198.4a1a08"]]},{"id":"a810262f.f17658","type":"http request","z":"ed332c3.038c7d","name":"GoogleMaps","method":"GET","ret":"obj","paytoqs":false,"url":"https://maps.googleapis.com/maps/api/directions/json?origin=StartStraße+Hausnummer,+PLZ+StartOrt&destination=ZielStraße+Hausnummer,+ZielOrt&departure_time=now&key=DeinAPI-Key&lang=de","tls":"","persist":false,"proxy":"","authType":"","x":550,"y":220,"wires":[["1ce8a57a.85be63"]]},{"id":"1ce8a57a.85be63","type":"function","z":"ed332c3.038c7d","name":"Minuten im Verkehr","func":"var seconds_in_traffic = msg.payload.routes[0].legs[0].duration_in_traffic.value;\nvar minutes_in_traffic = Math.round(seconds_in_traffic / 60);\n\nmsg.payload = {\"minutes_in_traffic\": minutes_in_traffic};\nreturn msg;","outputs":1,"noerr":0,"x":770,"y":220,"wires":[["7f28f525.a9daa4"]]},{"id":"33d49f89.f66c08","type":"http request","z":"ed332c3.038c7d","name":"OpenWeatherMap","method":"GET","ret":"obj","paytoqs":false,"url":"http://api.openweathermap.org/data/2.5/weather?id=OrtID&APPID=DeinAPI-Key","tls":"","persist":false,"proxy":"","authType":"","x":570,"y":260,"wires":[["59e07594.3e2a84"]]},{"id":"59e07594.3e2a84","type":"function","z":"ed332c3.038c7d","name":"Aktuelles Wetter","func":"var temp = Math.round(msg.payload.main.temp - 273.15);\nvar feel = Math.round(msg.payload.main.feels_like - 273.15);\nvar humi = msg.payload.main.humidity;\nvar wind = Math.round(msg.payload.wind.speed * 3.6);\n\n\nvar temp_text = null;\n\nif (temp <= -5) {\n temp_text = \"arschkalt\";\n} else if (temp <= 1) {\n temp_text = \"frostig\";\n} else if (temp <= 10) {\n temp_text = \"kalt\";\n} else if (temp <= 16) {\n temp_text = \"frisch\";\n} else if (temp <= 20) {\n temp_text = \"angenhem\";\n} else if (temp <= 28) {\n temp_text = \"warm\";\n} else if (temp <= 34) {\n temp_text = \"heiß\";\n} else if (temp <= 44) {\n temp_text = \"extrem heiß\";\n}\n\nmsg.payload = {\"temp\": temp, \"humi\": humi, \"wind\": wind, \"feel\": feel, \"temp_text\": temp_text};\nreturn msg;","outputs":1,"noerr":0,"x":780,"y":260,"wires":[["7f28f525.a9daa4"]]},{"id":"d9aab419.1a7a98","type":"template","z":"ed332c3.038c7d","name":"Siri Template","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"Die Fahrt zur Arbeit dauert aktuell {{payload.minutes_in_traffic}} Minuten. ... ... Draußen ist es {{payload.temp_text}}! ... Die Temperatur beträgt {{payload.temp}} Grad Celsius. ... Bei einer relativen Luftfeuchtigkeit von {{payload.humi}} Prozent ... und einer Windgeschwindigkeit von {{payload.wind}} KMH ... liegt die gefühlte Temperatur bei {{payload.feel}} Grad Celsius.","output":"str","x":990,"y":260,"wires":[["1891f113.c9ed2f"]]},{"id":"680685fe.174fac","type":"inject","z":"ed332c3.038c7d","name":"alle 10 Minuten","topic":"","payload":"","payloadType":"date","repeat":"600","crontab":"","once":true,"onceDelay":0.1,"x":140,"y":220,"wires":[["1191eb7f.c6a04d"]]},{"id":"7f28f525.a9daa4","type":"join","z":"ed332c3.038c7d","name":"","mode":"custom","build":"merged","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":true,"timeout":"","count":"2","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":970,"y":220,"wires":[["d9aab419.1a7a98"]]},{"id":"1891f113.c9ed2f","type":"function","z":"ed332c3.038c7d","name":"flow.set Siri","func":"flow.set('Siri',msg.payload);\nreturn msg;","outputs":1,"noerr":0,"x":990,"y":300,"wires":[[]]},{"id":"a437e365.2af878","type":"switch","z":"ed332c3.038c7d","name":"!=0","property":"payload.Siri","propertyType":"msg","rules":[{"t":"neq","v":"0","vt":"num"}],"checkall":"true","repair":false,"outputs":1,"x":510,"y":100,"wires":[["872b278d.eb5e1"]]},{"id":"545bc309.2efb94","type":"comment","z":"ed332c3.038c7d","name":"REST API Endpunkt","info":"","x":130,"y":60,"wires":[]},{"id":"ea786fea.e3c1e","type":"comment","z":"ed332c3.038c7d","name":"Siri Am Morgen","info":"","x":120,"y":180,"wires":[]},{"id":"1191eb7f.c6a04d","type":"function","z":"ed332c3.038c7d","name":"Aktuelle Stunde","func":"var date = new Date();\nvar hour = date.getHours();\nmsg.payload = {\"hour\": hour};\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":220,"wires":[["22c94e56.c61a8a"]]},{"id":"22c94e56.c61a8a","type":"switch","z":"ed332c3.038c7d","name":"5:00 bis 9:59 Uhr","property":"payload.hour","propertyType":"msg","rules":[{"t":"btwn","v":"5","vt":"num","v2":"9","v2t":"num"}],"checkall":"true","repair":false,"outputs":1,"x":350,"y":260,"wires":[["33d49f89.f66c08","a810262f.f17658"]]}]


    Nach einem Klick auf deploy können wir jetzt die URL unseres REST API Endpunkt im Browser aufrufen. Bei mir ist das http://dingleberry.local:1880/rest/siri_morgens - bei euch höchstwahrscheinlich eine andere.



    Wenn dort alles schick aussieht, können wir den Siri Kurzbefehl erstellen - und sind auch schon fast fertig.

    Siri Kurzbefehl

    Für den Siri Kurzbefehl müssen wir Inhalte von URL abrufen auswählen. Dort tragen wir als URL - wer hätte das gedacht - den Endpunkt unserer API ein. Als nächstes wollen wir den Wörterbuchwert abrufen und zum Schluss lassen wir uns den Text sprechen. Fettgedruckt sind hier jeweils die Namen der Befehle in der Kurzbefehle App.



    Abschließende Worte

    Den Kurzbefehl kann man zum Beispiel mit seinem Wecker verknüpfen und sich jeden Morgen top informiert auf den Weg zur Arbeit machen. Natürlich lässt sich der Funktionsumfang noch endlos erweitern. Über jede Anregung freue ich mich!


    Meinen besonderen Dank an sschuste - Du hast mich überhaupt erst auf Node-RED gebracht und Recht behalten, als du meintest, es würde mir Spaß bereiten.


    Natürlich auch ein großes DANKE an das gesamte Forum und die aktiven Mitglieder. Ihr seid einfach spitze! Ohne euch müsste ich bestimmt noch immer so altmodisches Zeug wie Lichtschalter benutzen 8o Ich bin gerne hier <3

  • Eine sehr gute Frage, auf die ich gerade keine Antwort weiß. :D


    Ich hab diese Morgenroutine auch mal eine Zeit lang direkt mit der Kurzbefehle App realisiert. Ab und an hatte mein iPhone aber Probleme mit der Ortung, was die Ausführung verzögert hat - oder der Kurzbefehl kam aus anderen, mir unbekannten Gründen zum stocken. Kann sein, dass es mittlerweile besser läuft.

  • Das wäre natürlich auch eine Option. :thumbup:


    War auch mehr als Beispiel gedacht, was mit Node-RED alles möglich ist. Zugegeben: wohl etwas unglücklich gewählt. :P


    Offtopic:


    Wenn ich aus der internen Testphase raus bin, werde ich hier wohl noch die ein oder andere Anleitung zum Thema HomeKit & Node-Red schreiben. Von ehemals 14 Homebridge-Plugins bin ich mittlerweile auf nur noch eins runter (config-ui-x zähl ich jetzt mal nicht mit). Der Rest läuft komplett über Node-RED mit dem NRCHKB-Modul. Alles sehr stabil und mit traumhaften Reaktionszeiten. Zudem komplett auf die eigenen Bedürfnisse anpassbar.


    Ich hab zum Beispiel einen Shelly RGBW2 direkt mit einem Zusatzschalter in HomeKit, der, sobald aktiviert, die entsprechende Leuchte blinken lässt (ja, ich weiß: das geht auch mit Kurzbefehlen;)) - was ich zur Zeit für die Türklingel nutze. In HomeKit sieht das so aus:



    Aber wie gesagt: noch in der Testphase. Und jetzt genug Offtopic.


    Schönes Wochenende,

    Gerrit