Eine eigene PHP-Web-API bauen

Um Daten aus dem SmartHome auf eine eigene Website zu bringen, habe ich mir eine Authentifizierung gebaut, die es mir erlaubt, per POST Request Daten bereitzustellen – und zwar nur mir. Einfach nur ein festes Passwort war mir da zu unsicher. Daher habe ich eine zeitbasierte Komponente mit eingebaut.

Zum Einsatz kommt seitens Node-RED node-red-contrib-cryptography.

Ich generiere einen Auth Token, dieser basiert auf der aktuellen Zeit und einem geheimzuhaltenden Passwort, quasi einem Salt.

msg.payload = Math.round(Date.now()/10000) + "dasGeheimePasswort1234!";
msg.headers = {};
msg.headers["Content-Type"] = "application/x-www-form-urlencoded";
return msg;

Beim Token habe ich mich für eine Gültigkeit von 10 Sekunden (daher geteilt durch 10.000) entschieden, dann sind minimale Ungenauigkeiten bei den Uhrzeiten zwischen SmartHome und Webserver nicht so dramatisch. Der Payload wird dann durch die hash-Funktion sha256 gehasht. Anschließend wird der POST Request mit Daten bepackt und der http request abgeschickt.

msg.payload = {
"auth": msg.payload,
"temperatur": msg.temperatur
};
return msg;

Um dem POST Request zu empfangen und zu authentifizieren muss die PHP Gegenstelle den gleichen Token generieren und mit dem mitgeschickten Token abgleichen. Dazu braucht die Gegenstelle natürlich auch den auf 10 Sekunden gerundeten Timestamp und das gleiche geheime Passwort, wie es im Node-RED hinterlegt ist.

<?php
   if($_POST["auth"] == hash('sha256',(round(time()/10))."dasGeheimePasswort1234!")) {
echo "true";
}
?>

Achtung! PHP arbeitet bei den Timestamps in Sekunden, während JavaScript in Millisekunden arbeitet. Daher ist hier nur eine Division durch 10 nötig.

Statt dem echo „true“; kann man natürlich beliebiges mit den mitgeschickten Werten anfangen.

Wetter selbst von yr.no abrufen

Update vom 12.01.2022

Jetzt mit Details zu Dashboard-Ausgabe, inkl. Vorhersage-Chart.

Wetterdaten fürs SmartHome

Rund ums Wetter sammle ich mir eine Vielzahl an Daten aus verschiedenen Quellen. Die Wettervorhersage beziehe ich vom Norwegischen Wetterdienst yr.no, Live-Wetterdaten erhebe ich selbst oder hole sie mir über openSenseMap und Weather.com (Weather Underground) von Wetterstationen aus der Nachbarschaft. Wetterwarnungen kommen vom DWD und ein schickes Niederschlagsradar vom Wetterdienst eines (ehemals) bekannten Wetter-Moderators.

Vorgeschichte

Bisher habe ich für die Wettervorhersage via yr.no den ioBroker-Adapter genutzt. Doch seit der Umstellung der API von xml auf json ist die Anzahl der verfügbaren Werte und damit auch die Anzahl der ioBroker-Objekte explodiert – vieles davon ohne Mehrwert für mich. Dies war mir ein Dorn im Auge, versuche ich doch die Anzahl der Schreibvorgänge auf die SD-Karte klein zu halten.

Also wollte ich die API von yr.no/met.no selbst anzapfen, mit Node-RED ja kein Problem.

So mach ich es

Wenn man die API-Dokumentation aufmerksamer ließt als ich, kann man das ganze recht schnell durchschauen. (Ich brauchte dementsprechend eine Weile.) Man hat die Wahl zwischen dem classic, compact und complete Bericht. Mich interessieren die Temperaturen, die Wetterlage und die erwarteten Niederschlagsmengen. Diese bekomme ich im complete-Bericht.

Man benötigt seine Position als lat/lon. Google Maps verrät beim Rechtsklick auf jeden beliebigen Punkt der Erde die entsprechenden Werte, der erste ist dabei der Breitengrad (lat), der zweite der Längengrad (lon). Ergänzen kann man noch die Höhe über Meeresspiegel – dies ist jedoch optional.

Das Query-String für den Erfurter Dom wäre dann

https://api.met.no/weatherapi/locationforecast/2.0/complete?altitude=194&lat=50.97578937177409&lon=11.023335426472794

Es ist nötig, einen eigenen User-Agent als Identifikation bei der Anfrage mitzusenden, sonst bekommt man einen Fehler „403 Forbidden“. Hier kann man beliebig kreativ sein. Das setzen des Headers geht ganz einfach mit einem Function-Node vor dem HTTP-Request-Node:

msg.headers = {
    "User-Agent": "erfurter-dom-99084"
}
return msg;

Wenn man alles richtig gemacht hat, erhält man als Rückgabe einen wirklich umfangreichen JSON-Datensatz. Dieser enthält Vorhersagedaten für die nächsten 9 Tage. Mich interessieren tatsächlich nur die nächsten 48 Stunden. Auf weiterreichende Wetterentwicklung reagieren meine Flows nicht, und ich habe nicht den Elan, ein allzu umfangreiches Wetter-Dashboard zu bauen. Außerdem ist mir eine weitere Vorhersage eh zu sehr Wahrsagerei.

Die Auswahl der Daten

Da man die API nicht zu oft abfragen darf (und auch nicht will), sollte man die Vorhersagedaten bis zum nächsten API-Abruf zwischenspeichern. Dazu muss man sich überlegen, welche Daten man benötigt.

Beim Stöbern im JSON-Datensatz bin ich darauf gekommen, dass die interessantesten Daten für mich im Bereich der „next_6_hours“ stecken. Sowohl der Symbolcode (= die Wetterlage), als auch die Min-/Max-Temperaturen und die Niederschlagsmenge werden für diese Zeit-Korridore mitgeliefert – und das anfangs stündlich.

{
        "time": "2021-08-30T15:00:00Z",
        "data": {
          "instant": {
            "details": {
              "air_pressure_at_sea_level": 1018.5,
              "air_temperature": 13.8,
              "cloud_area_fraction": 100,
              "cloud_area_fraction_high": 57.8,
              "cloud_area_fraction_low": 100,
              "cloud_area_fraction_medium": 28.1,
              "dew_point_temperature": 12.9,
              "fog_area_fraction": 0,
              "relative_humidity": 94.4,
              "ultraviolet_index_clear_sky": 0,
              "wind_from_direction": 355.9,
              "wind_speed": 2.8
            }
          },
          "next_12_hours": {
            "summary": {
              "symbol_code": "lightrain"
            }
          },
          "next_1_hours": {
            "summary": {
              "symbol_code": "rain"
            },
            "details": {
              "precipitation_amount": 0.3
            }
          },
          "next_6_hours": {
            "summary": {
              "symbol_code": "lightrain"
            },
            "details": {
              "air_temperature_max": 13.9,
              "air_temperature_min": 13.1,
              "precipitation_amount": 0.6
            }
          }
        }
      },

(Nicht verwirren lassen! Alle Zeitangaben sind in UTC)

Die Logik dabei ist:

Zum Zeitpunkt 30.08.2021 17:00 Uhr schaue ich mit „next_6_hours“ auf den Zeitraum zwischen 17:00 und 23:00 Uhr. Auch für den Zeitpunkt 30.08.2021 18:00 Uhr gibt es die „next_6_hours“, welche sich dann auf den Zeitraum 18:00 bis 0:00 Uhr beziehen.

Auf Kosten der Genauigkeit (die bei einer Vorhersage eh relativ ist) und zugunsten der Datensparsamkeit speichere und verarbeite ich also nur jeden sechsten Datensatz bis 42 Stunden in die Zukunft (+6 hours ergibt die 48 Stunden die ich betrachten möchte).

Für diese acht Datensätze (8*6=48 Stunden) habe ich acht Objekte im ioBroker angelegt (bei mir n0, n6, n12, n18, …), in welche ich die inzwischen von JSON in ein JavaScript Objekt konvertierten Daten der „next_6_hours“ als Objekt abspeichere.

Datenaufbereitung

Vor dem Speichern bereite ich die Daten noch ein wenig auf. Ich ordne den Symbolcodes zusätzlich die deutschen Bezeichnungen der Wetterlage zu, ergänze den Wochentag und bereite die Zeit auf, indem ich den Zeitraum errechne, für den diese Werte eine Gültigkeit haben (z.B. Dienstag, 06 – 12 Uhr). Zudem wird der Timestamp des Gültigkeitsstarts errechnet und mit abgelegt.

Nutzung der Wetterdaten

In definierten Regeln

Meine Vorhersage-Objekte (n0 – n42) sind immer aktuell, da ich die Wetterdaten einmal die Stunde abrufe. Somit weiß ich immer, im welchen Objekt welche Zeiträume stecken. Je nachdem wann ein Flow das Wetter abfragt, muss ich also nur den entsprechenden Betrachtungszeitraum festlegen.

Schaut die Rollo-Steuerung zum Beispiel um 9 Uhr, ob sie die südlichen Fenster abschatten sollte, dann schaut sie u.a. nach den Höchstwerten von „n0“, also wie warm es maximal zwischen 9 und 15 Uhr wird. Soll das Haus am Abend Bescheid sagen, dass die Fenster dank kühler Abendluft endlich geöffnet werden können, dann schaut es erst, wie warm es am nächsten und übernächsten Tag wird („n18“, „n42“).

Im Dashboard / UI

Natürlich schauen nicht nur automatische Steuerungen nach dem Wetter – ich gebe die Vorhersage auch im Dashboard aus. Neben der noch ausbaubaren tabellarischen Aufbereitung habe ich dafür inzwischen auch ein Temperatur-Vorhersage Chart.

Da ich die Daten direkt als JavaScript Objekt in das ioBroker Objekt abspeichere, kann ich auch direkt nach dem Auslesen des Objekts auf die Werte zugreifen.

msg.payload.details.air_temperature_max

Die tabellarische Ausgabe basiert somit auf einfachen Abfragen und Textausgaben, da alle Daten ja schon passend aufbereitet wurden.

Die entsprechenden ui_text – Nodes, welche durch CSS Klassen etwas gestaltet werden:

1. und 2. Spalte (n0)
3. Spalte (w0)

Für das Temperatur-Vorhersage-Diagramm müssen alle Objekte (iobroker list Node) geladen und daraus die passende Chart-JSON generiert werden. (siehe Flow Download)

Beispielhafte JSON zur Befüllung eines Node-RED Dashboard Charts

[{"series":["min °C","max °C"],"data":[[-0.6,-0.5,3.7,1.9,0.5,0.6,3.2,3.6],[1,2.5,5.8,3.8,1.3,2.1,5,4]],"labels":[1,7,13,19,1,7,13,19]}]

Da der abzubildende Zeitraum in der Zukunft liegt, können die Werte nicht anhand eines timestamps verortert werden, sondern brauchen feste Bezeichnungen. Hierbei habe ich mich für die Mitte des sechs Stunden Zeitraums entschieden.

Flows als Download

Wer es ausprobieren möchte, braucht im ioBroker unter 0_userdata.0.Vorhersage entsprechende Objekte und kann nach dem Import des folgenden Flows direkt loslegen.

json auf Gültigkeit prüfen

Es ist mal wieder soweit – die Schnittstelle von opensensemap.org streikt. Da dies immer mal wieder (nicht nur dort) vorkommt, habe ich vor einiger Zeit eine Prüfung des JSON-Response eingebaut, sodass ich diese Fehler abfangen kann.

Damit ich dieses Code-Snippet nicht immer suchen muss, sei es hier archiviert. Vielleicht hilft es jemandem anders ja auch weiter.

var json_valid = true;

try
{
   var json = JSON.parse(msg.payload);
}
catch(e)
{
  json_valid=false;
}
finally {
    if(json_valid === true) {
        node.status({fill:"green",shape:"dot",text:"Okay"});
        return [msg,null];
       
    }
    else {
        node.status({fill:"red",shape:"dot",text:"Error"});
        return [null,msg];

    }
}

POST-HTTP Request mit Daten

Da ich mich sehr dumm angestellt habe und ewig brauchte, um mit Node-RED einen HTTP Request inkl. Daten zu versenden, dokumentiere ich hier – nur für mich (weil sich bestimmt niemand so umständlich anstellt) – die funktionierende Lösung.

Der simple Aufbau, ein Function-Node und ein HTTP-Request-Node:

Im Function-Node werden die zu übermittelnden Daten in msg.payload als JSON geschrieben. Außerdem wird
msg.headers["Content-Type"] = "application/x-www-form-urlencoded";
gesetzt.

Der HTTP-Request ist dann kaum zu konfigurieren. Methode = POST eintragen, dann noch die URL und fertig.

Warum ich meine Shelly 2.5 nicht mehr mit MQTT steuere

Bisher habe ich die Shelly 2.5, welche ich zur Rolladensteuerung einsetze, über iobroker + MQTT gesteuert. Ich nutze bei ihnen die original Firmware, weil ich die Positionskontrolle, die Kalibierierung und die Hindernis-Erkennung sehr schätze. Darum steuere ich jetzt nicht mehr per MQTT:

Scheinbar ein Firmware Bug

Disclaimer: dieser Artikel ist schon etwas älter. Ob es ein Bug war und ob es diesen potentiellen Bug noch gibt, weiß ich nicht. Ich komme mit der Nicht-MQTT Steuerung sehr gut klar, daher habe ich dies beibehalten.

Ab und zu kommt es bei meinen Shellys dazu, dass die WLAN Verbindung abbricht. Dies liegt einfach an der Reichweite des WLANs, die in mancher Wand im Grenzbereich liegt. Dies ist normalerweise kein Problem, da sich die Shellys recht zuverlässig wieder mit dem Router verbinden. Doch manchmal öffnen sich nach der Wiederverbindung wie von Geisterhand die Rollos – also sie fahren unvermittelt nach oben. Viele Tests später ist meine Theorie, dass dies am MQTT liegt, und es hier scheinbar einen Bug in der Firmware gibt.

MQTT + iobroker Objekte

Zweiter Grund: Von NodeRED kommt die Logik meines SmartHomes. So auch die Rolladensteuerung. Über das Setzen/Ändern von iobroker Objekten, sendet man die Befehle per MQTT. Ein Problem: hat das Objekt bereits den gleichen Wert, wie den, den man setzen will, wird der Befehl nicht übertragen. Wurde also zum Beispiel zuletzt das Rollo auf 60% gestellt, das Rollo danach per Wandtaster wieder hochgefahren, kann man es nicht erneut auf 60% einstellen, weil das Befehls-Objekt noch auf 60 steht. Man muss also umständlich auslesen, welcher Wert gerade im Objekt steht, und ggf. z.B. 61 übermitteln.

Darum nutze ich jetzt die HTTP API

Umgestiegen bin ich auf die HTTP API. Über sie rufe ich peridoisch den aktuellen Status ab (tags öfter als nachts) und sende Befehle. Die Vorteile:

  • Ich habe einen Adapter weniger im iobroker, spart Arbeitsspeicher.
  • Ich umgehe den möglichen Firmware Bug und habe (bisher) keine geisterhaft öffnenden Rollos mehr.
  • Ich kann Befehle senden, ohne diese vorher mit dem aktuellen Status abgleichen zu müssen.
  • Ich bekomme über den HTTP StatusCode (200) direktes Feedback, ob ein Befehl angekommen ist.

So mache ich es

Zum einen habe ich in Node-RED einen HTTP in-Node erstellt, bei dem sich die Shellys über die I/O URL Actions melden, wenn sie stoppen – also die Rollos eine neue Position eingenommen haben. Der dann angestoßene Abruf der Position hält den Wert im iobroker aktuell.

Zum anderen habe ich einen Subflow erstellt, welcher die Befehle an die Shellys übermitteln. Er benötigt als Argument die IP des betroffenen Shellys und einen Befehl. Dabei verarbeitet der Subflow sowohl Open, Close und Stop als Textbefehle als auch Integer-Werte – zwischen 0 und 100 – für die Position des Rollos in „Prozent geöffnet“. (100% = ganz offen)

Hier das Flow: