Home-Automatisierungslösungen wie FHEM sind mir eigentlich zu mächtig. Außerdem können die schon von Haus aus zu viel, man ist eher Administrator als Entwickler. Außerdem macht mir das Frickeln und Lernen uneimlich viel Spaß.
Für meine bescheidenen Anwendungsfälle reicht ein grafisches Universaltool wie Node -Red, das ideal für einen Microserver wie den Raspberry Pi geeignet ist. Es läuft sogar problemlos auf einem Raspberry Pi Zero der ersten Generation.
Inhalt
Was brauchen wir?
- Einen Raspberry Pi mit installiertem Webserver, PHP und einer aus dem Internet erreichbaren URL
- darauf installiertem Node-Red
- Für die echte Anwendung: eventuell Schaltaktoren z.B. von Shelly sowie einen MQTT Server z.B. Mosquitto
Geradeaus mit html Node
Ich gehe davon aus, dass du Node-Red installiert hast und dich schon etwas mit den Grundzügen auskennst.
Für's Erste arbeiten wir direkt mit Locative und Node-Red, ohne PHP Klimbim.
Nachrichten im Node-Red Teststub empfangen
Dann basteln wir uns mal einen Primitiv-Flow bestehend aus einem http-in Node, einem Template- sowie einem http-out node zur Response und zum Ansehen der Daten einem Debug Node:
Anbei JSON zum importieren
1 |
[{"id":"c7b69f70141df5bd","type":"tab","label":"PHP-POST Remote","disabled":false,"info":"","env":[]},{"id":"747f98cb845836e5","type":"debug","z":"c7b69f70141df5bd","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":420,"y":240,"wires":[]},{"id":"9c4d9ad0982bbaae","type":"http in","z":"c7b69f70141df5bd","name":"","url":"testman","method":"post","upload":false,"swaggerDoc":"","x":180,"y":240,"wires":[["747f98cb845836e5","77a737f18c60340f"]]},{"id":"77a737f18c60340f","type":"template","z":"c7b69f70141df5bd","name":"Response","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"Daten sind angekommen","output":"str","x":420,"y":180,"wires":[["0c817e6bb44a55a3"]]},{"id":"0c817e6bb44a55a3","type":"http response","z":"c7b69f70141df5bd","name":"","statusCode":"","headers":{},"x":650,"y":180,"wires":[]}] |
Der http In Node wird konfiguriert, indem bei URL eine wahlfreie Zieladresse (hier testman) eingetragen wird. Diese URL ist dann erreichbar unter
http://NameDesServers:1880/testman
Node-Red bringt seinen eigenen Webserver mit, der unter der Portnummer 1880 erreichbar ist. Will man die Node-Red Applikation über das Internet erreichbar machen, muss im Router die Portnummer 1880 nach draußen durchgereicht, also freigeschaltet werden. Damit hat man dann ein Loch groß wie ein Scheunentor in seine Firewall gehauen. Doch dazu später mehr.
Biegen wir unsere Webhookadresse in der Locative App auf die neue Zieladresse um, werden die geschickten Daten brav entgegengenommen, im Debug Fenster rechts (Käfersymbol) angezeigt und mit "Daten sind angekommen" beantwortet.
Sehr schön, keine 5 Minuten hat das Entwickeln gedauert…
Auf den Trigger kann man direkt über msg.payload.trigger zugreifen.
Lichtsteuerung
So, jetzt wollen wir noch etwas Richtiges mit Node-Red anfangen: Wenn ich nach Sonnenuntergang – bzw. vor Sonnenaufgang heimkomme, soll das Licht angehen:
Ein paar Erklärungen: Den Astrodata day values Node werdet ihr wahrscheinlich nachinstallieren müssen. Leider gibt der Node keine Unix Timestamps aus, sondern echte Uhrzeiten, weshalb sie hinterher im Day or Night Function Node noch umgerechnet werden müssen. So wird sichergestellt, dass zwischen Sonnenauf und –untergang das Licht nicht eingeschaltet wird.
Wie schon beschrieben, arbeite ich gerne mit Shelly Aktoren, die über MQTT angesteuert werden. So passiert alles über WiFi, irgendwelche Philips Hue oder sonstige Hubs brauche ich nicht.
Und hier das JSON:
1 |
[{"id":"7a50cffc5bf09d25","type":"tab","label":"Flow 1","disabled":false,"info":"","env":[]},{"id":"a077b0897e8725c2","type":"function","z":"7a50cffc5bf09d25","name":"Day or Night","func":"var rTime = msg.sunRise;\nrTime = rTime.split(\":\");\nvar hours = parseInt(rTime[0]);\nvar mins = parseInt(rTime[1]);\nvar secs = parseInt(rTime[2]);\nrTime = hours * 3600 + mins * 60 + secs;\n\nvar sTime = msg.sunSet;\nsTime = sTime.split(\":\");\nhours = parseInt(sTime[0]);\nmins = parseInt(sTime[1]);\nsecs = parseInt(sTime[2]);\nsTime = hours * 3600 + mins * 60 + secs;\n\nvar aTime = new Date();\nhours = aTime.getHours();\nmins = aTime.getMinutes();\nsecs = aTime.getSeconds();\naTime = hours * 3600 + mins * 60 + secs;\n\nif (rTime <= aTime && aTime <= sTime) \n {msg.payload = \"day\";}\nelse\n {msg.payload = \"night\";}\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":770,"y":500,"wires":[["53757a5dd04a31e5"]]},{"id":"f165134d4df1475a","type":"debug","z":"7a50cffc5bf09d25","name":"enter","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":600,"y":540,"wires":[]},{"id":"370b5f0349a479ce","type":"http in","z":"7a50cffc5bf09d25","name":"","url":"lightman","method":"post","upload":false,"swaggerDoc":"","x":260,"y":540,"wires":[["2b1ba4c3062bff09"]]},{"id":"2c43054e86ea35e0","type":"http response","z":"7a50cffc5bf09d25","name":"response","statusCode":"","headers":{},"x":1320,"y":500,"wires":[]},{"id":"c70704402596a1be","type":"template","z":"7a50cffc5bf09d25","name":"Nighttime Message ","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"Entering area - nighttime. Command executed.","output":"str","x":1110,"y":540,"wires":[["2c43054e86ea35e0"]]},{"id":"53757a5dd04a31e5","type":"switch","z":"7a50cffc5bf09d25","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"day","vt":"str"},{"t":"eq","v":"night","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":920,"y":500,"wires":[["dbfb6da8ffbdfa6e"],["c70704402596a1be","4557f7131cdab0f2"]]},{"id":"dbfb6da8ffbdfa6e","type":"template","z":"7a50cffc5bf09d25","name":"Daytime Message","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"Entering area. Daytime, no need for lights...","output":"str","x":1110,"y":480,"wires":[["2c43054e86ea35e0"]]},{"id":"4557f7131cdab0f2","type":"change","z":"7a50cffc5bf09d25","name":"Send ON command","rules":[{"t":"set","p":"payload","pt":"msg","to":"on","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":980,"y":640,"wires":[["9748e9698d6fddfd"]]},{"id":"2e179d331123df50","type":"astrodata dayvalues","z":"7a50cffc5bf09d25","name":"sun up/down","lon":"11.542510","lat":"48.148691","height":"600","lang":"de","offset":0,"x":610,"y":500,"wires":[["a077b0897e8725c2"]]},{"id":"2b1ba4c3062bff09","type":"switch","z":"7a50cffc5bf09d25","name":"enter/exit","property":"payload.trigger","propertyType":"msg","rules":[{"t":"eq","v":"enter","vt":"str"},{"t":"eq","v":"exit","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":430,"y":540,"wires":[["2e179d331123df50","f165134d4df1475a"],["1aa9c0085172028f"]]},{"id":"aefe93299f2ea6d6","type":"http response","z":"7a50cffc5bf09d25","name":"response","statusCode":"","headers":{},"x":750,"y":600,"wires":[]},{"id":"1aa9c0085172028f","type":"template","z":"7a50cffc5bf09d25","name":"","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"Leaving perimeter... ","output":"str","x":590,"y":600,"wires":[["aefe93299f2ea6d6"]]},{"id":"9748e9698d6fddfd","type":"mqtt out","z":"7a50cffc5bf09d25","name":"Licht Haustüre","topic":"shellies//haustuere/relay/0/command","qos":"2","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"c3ec39dc.4614f","x":1200,"y":740,"wires":[]},{"id":"2c0b6399243d0a5f","type":"ui_switch","z":"7a50cffc5bf09d25","name":"","label":"Haustüre","tooltip":"","group":"eb55fbe3.b2bcd8","order":1,"width":0,"height":0,"passthru":false,"decouple":"true","topic":"","style":"","onvalue":"on","onvalueType":"str","onicon":"","oncolor":"","offvalue":"off","offvalueType":"str","officon":"","offcolor":"","x":1000,"y":740,"wires":[["9748e9698d6fddfd"]]},{"id":"9d6a6d8fcf352951","type":"mqtt in","z":"7a50cffc5bf09d25","name":"","topic":"shellies/haustuere/relay/0","qos":"2","datatype":"auto","broker":"c3ec39dc.4614f","nl":false,"rap":false,"inputs":0,"x":770,"y":740,"wires":[["2c0b6399243d0a5f"]]},{"id":"c3ec39dc.4614f","type":"mqtt-broker","name":"CentralinaPi","broker":"192.168.1.25","port":"1883","clientid":"","autoConnect":true,"usetls":false,"compatmode":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"sessionExpiry":""},{"id":"eb55fbe3.b2bcd8","type":"ui_group","name":"Außenlicht","tab":"5807093d.850828","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"5807093d.850828","type":"ui_tab","name":"Lichtsteuerung","icon":"dashboard","order":1,"disabled":false,"hidden":false}] |
Sicherheitsrisiko
Wie schon erwähnt, würdet ihr mit der Port-Freischaltung der Node-Red Portadresse 1880 im Router ein riesiges Sicherheitsproblem bekommen. Praktisch jeder, der die URL und die Portnummer kennt, könnte eure schönen Node-Red Logiken fernsteuern.
Ich habe auch noch nicht herausgefunden, wie sich das Dashboard passwortschützen lässt. Bei einem maschinellen Zugriff würde das eh nicht viel helfen.
Allermindestens müsst ihr den Editor mit Passwort schützen.
Es gibt auch noch einen installierbaren Node mit Namen "basic http auth", der funktioniert ganz gut, schützt aber nur den Flow, in dem ihr ihn einbauen könnt. Andere Flows im Dashboard bleiben ungeschützt.
Trotzdem gibt es für dieses Problem eine Lösung — wie ich meine…
Maskieren mit Proxy
Mein Setup sieht wie folgt aus:
- Raspberry Pi als "Universalserver" z.B. für dieses Blog hier, für meine Nextcloud und diverse andere Funktionen, z.B. als Gateway für ein paar Reverse SSH Tunnel. Jetzt auch als Server für Node-Red.
- Nur die Ports 80 und 443 sind im Router freigeschaltet, damit der Webserver öffentlich erreichbar ist.
- Port 1880 ist nicht freigeschaltet. Node-Red kann also nur intern, innerhalb des privaten Netzes aufgerufen werden.
Die Lösung besteht nun darin, die bei Port 80 bzw. 443 ankommenden, für Node-Red bestimmten Befehle, intern auf Port 1880 umzuleiten. Natürlich nur dann, wenn sich der User bzw. Locative oder andere Tools mit http Basic Authorization authentifiziert haben.
Ich habe nun das in Kapitel 1 vorgestellte PHP Skript mit Basic Authentification übernommen, und um eine Forwarding Komponente (gelb markiert) erweitert. Es fungiert gewissermaßen als Proxy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
//forwarder.php <?php //debug setting error_reporting (E_ALL | E_STRICT); ini_set ('display_errors' , 1); // Ende Debug //check if exists basic auth in request if (isset($_SERVER["HTTP_AUTHORIZATION"])) { $auth = $_SERVER["HTTP_AUTHORIZATION"]; $auth_array = explode(" ", $auth); $un_pw = explode(':', base64_decode($auth_array[1])); $un = $un_pw[0]; $pw = $un_pw[1]; // check Uname & PW $fh = file_get_contents('.password/pwds.env'); $pline = explode(':', $fh); $encpw = preg_replace('/\s+/', '', $pline[1]); $verify = password_verify($pw, $encpw); // if password is correct, continue if ($un == $pline[0] && $verify) { //receiving part $request = file_get_contents('php://input'); //$req_dump = print_r( $request, true ); $fp = file_put_contents( 'request.log', $request ); //forwarding part $ch = curl_init(); curl_setopt($ch, CURLOPT_URL,"http://localhost:1880/lichtan"); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $request); // receive server response and send back to locative app curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $server_output = curl_exec ($ch); print_r($server_output); curl_close ($ch);} else { echo "UNAUTHORIZED!!!!!";} } else { echo "Missing credentials";} ?> |
Die von einem autorisierten Nutzer an eine öffentliche Adresse (meinen Webserver) gesendeten Daten werden nun intern im privaten Netz, abgekoppelt vom großen und weiten Internet, mit cURL an den auf derselben Maschine (localhost) installierten, nicht-öffentlichen Node-Red Webserver weitergeleitet. Der dort existierende http in Node lauscht auf der URL serverip:1880/lichtan (bzw. localhost:1880/lichtan) und arbeitet wie gewünscht. Meiner Meinung nach ist das eine sichere Lösung, oder?
Natürlich muss Locative auch entsprechend auf die öffentliche Adresse umgestellt werden, um das php Skript – hier: forwarder.php – aufzurufen. In die URL Zeile wird also sinngemäß https://meineDynIPAdresse/webhook/forwarder.php eingetragen.
Weltweite Aktionen
Das ganze lässt sich noch mit Minimalaufwand auf eine globale Lösung ausweiten:
Einer meiner beliebtesten (und ältesten) Artikel ist Reverse SSH Tunnel – Schritt für Schritt. Hat man so ein Reverse Tunnel realisiert um z.B. auf einen Raspberry Pi oder eine Webcam im Ferienhaus zuzugreifen, muss der Remote Rechner im Ferienhaus nur die Portadresse 1880 per Reverse SSH Tunnel auf eine freie Portadresse im Gateway Server z.B. 2300 umleiten. 2300 ist im Router nicht freigeschaltet.
Anstatt
curl_setopt($ch, CURLOPT_URL,"http://localhost:1880/lichtan");
schreibt man im Skript forwarder.php
curl_setopt($ch, CURLOPT_URL,"http://localhost:2300/lichtan");
Will sagen: An Stelle der Adresse des internen Node-Red Webservers, gibt man die interne Weiterleitungsportnummer des Gateways an. Die Bytes kommen am Remote Server an und landen schlussendlich am dortigen Node-Red Port 1880. Fertig!
Bei Interesse gerne fragen. Ich erkläre es dann. Ich habe seinerzeit mehrere Wochen gebraucht, mein Hirn um dieses Thema herumzuwinden.