Tür öffnen mit Siri und OTP – neue Version

Nach ein paar Wochen Betrieb meines OTP-Türöffners https://vc4.de/tuer-oeffnen-mit-otp-und-siri-shortcuts/ habe ich über ein paar Optimierungen – insbesondere bei den Siri-Shortcuts nachgedacht.

Zunächst habe ich entdeckt, dass die meisten OTP-Apps die Möglichkeit bieten, das OTP in die Zwischenablage zu kopieren. Also habe ich die erste Aktion „Gib den Zugangscode ein“ ersetzt durch „Zwischenablage abrufen“ und dann in die nächste Aktion durch „Variable sOTP auf Zwischenablage festlegen“ ersetzt. Das spart dann zumindest mal einen Schritt.

Und danach dachte ich mir: Vielleicht gibt es eine Shortcut-fähige OTP App, die dann in der Lage ist, per Siri Shortcut einen Code direkt in die Zwischenablage zu kopieren. Und siehe da: Die gibt es tatsächlich.

Auf Reddit gibt es eine Liste der Shortcut-fähigen Apps: https://www.reddit.com/r/shortcuts/comments/9rd56b/running_list_of_apps_that_supports_siri_shortcuts/

Und da findet sich die App OTP Auth von Roland Moers: https://apps.apple.com/us/app/otp-auth/id659877384

Mehr Infos zur App auch auf der Webseite von Roland Moers hier: https://cooperrs.de/otpauth.html

Nun sieht der erste Teil meines Kurzbefehls so aus:

Und damit brauche ich außer dem Aufruf des Shortcuts keine weitere Interaktion mehr mit meinem iPhone. Und das coolste war das Gesicht der Personen, die mich vor der Tür stehend haben sagen hören „Hey Siri, Tür öffnen“. Der Blick: Unbezahlbar!

P.S.: Die 4,49 € für die Pro Version von OTP Auth habe ich gerne bezahlt auch wenn ich die eigentlich nicht gebraucht hätte. Danke Roland.

Tür öffnen mit OTP und Siri-Shortcuts

Kurzer Disclaimer vorweg: Alles was ihr jetzt lest und ggf. nachmacht, macht ihr auf eigene Gefahr. Die hier vorgestellte Lösung ist dazu geeignet, dass ihr den Inhalt eueres Hauses verlieren könnt. Wenn ihr irgendetwas davon nachmachen wollt, solltet ihr sicher sein, dass das was ihr hier lest, auch komplett verstanden und von euch nachvollzogen werden kann. Ich hab‘ euch jedenfalls gewarnt!

Was wird benötigt, um das ganze zu verstehen?

  • Fundierte FHEM-Kenntnisse
  • Eine gegenüber dem Internet abgeschottete FHEMWEB-Instanz
  • FHEM kann aus dem Internet über DynDNS erreicht werden
  • Ein iOS Gerät mit der Kurzbefehle-App und Kenntnisse, wie man einen Siri-Shortcut erstellt
  • Programmierkenntnisse in der Arduino-IDE (oder vergleichbaren Lösungen)
  • Einen MQTT-Broker und Kenntnisse, wie das MQTT Protokoll funktioniert
  • Einen WeMos D1 Mini und ein wenig Verständnis, wie der funktioniert
  • Ein kleines SG90 Servo (zumindest in meinem speziellen Fall, aber dazu später)
  • Ein wenig „vorhandene“ Hardware z.B. Breadbord, Jumperwires usw. was man als Maker so eben braucht

So und jetzt geht’s los.

Seit einigen Jahren setze ich FHEM zur Hausautomatisierung ein. Seit den ersten Tagen habe ich eine Möglichkeit gesucht, über FHEM meine Haustür öffnen zu können. Und das möglichst charmant und natürlich auch sicher. Die Haustür hat einen elektrischen Türöffner, der über ein GIRA Steuergerät Audio 1287 00 und entsprechende Türsprechstellen bzw. Wohnungsstationen ausgelöst werden kann.

Leider gab es hier immer zwei kleinere Hürden zu überwinden:

Das Steuergerät von GIRA wird über ein Bus-System gesteuert, zu dem es – zumindest für mich – keine Protokollbeschreibungen gibt. Außerdem kann ich an den Wohnungsstationen nur dann die „Tür öffnen-Taste“ bedienen, wenn vorher jemand an der Türstation geklingelt hat… Irgendwie in diese vorhandenen Geräte einzugreifen und über Relais, Optokoppler oder sonstwas die Tür zu öffnen und dabei das komplette „System“ zu gefährden, kam also nicht in Frage.

Die zweite Hürde war, das ganze irgendwie gegen Missbrauch abzusichern. Natürlich kann man in FHEM Befehle ja über einen URL-Aufruf per http im Browser ausführen. Aber FHEM ungesichert aus dem Internet erreichbar zu machen? No way! Die Instanz mit einem Benutzer und einem komplexen Passwort abzusichern? Schon besser. HTTPS verwenden über einen Reverse-Proxy und let’s encrypt Zertifikaten? Wir kommen dem Ziel schon näher.

Doch zunächst zur ersten Hürde zurück. Zum Glück gibt’s von dem Steuergerät eine Bedienungsanleitung in der folgendes steht:

Die Taste „Türöffnerprogr.“ hat zwei Funktionen:

  1. Türöffner-Programmiermodus einschalten:
    Wird die Taste „Türöffnerprogr.“ für 3 s gedrückt, während sich das System im Programmiermodus befindet, wird der Türöffner-Programmiermodus aktiviert … blablabla
  2. Betätigung des angeschlossenen Türöffners.
    Durch kurzes Drücken der Taste „Türöffnerprogr.“ wird der Türöffner für die eingestellte Zeit (Türöffnerzeit) aktiviert…

Aha! Der zweite Punkt lässt sich also mit ein wenig Hardware lösen: Ein kleines Servo gesteuert von einem WeMos D1 mini sollte doch diese kleine Taste betätigen können. Und der WeMos wäre ja auch nicht der erste, der sich im Schaltschrank befindet. Ein wenig Recherche im Netz und der Code für die Servosteuerung über MQTT war fertig:

/*
Wifi, MQTT und Servo
*/

#include <ESP8266WiFi.h>
#include <PubSubClient.h>

#include <Servo.h> 
Servo myservo; 

// Update these with values suitable for your network.

const char* ssid = "SSID";
const char* password = "WLANPASSWORT";
const char* mqtt_server = "MQTTBROKER"; // MQTT IP oder DNS-Name
const char* servo_topic = "/servo/cmd"; // Topic braucht ihr später noch in FHEM
 
WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
char msg[50];
int value = 0;

void setup() {
  Serial.begin(115200);
  myservo.attach(2);
  myservo.write(value);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

void setup_wifi() {

  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
   String string;

  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
   // Serial.print((char)payload[i]);
     string+=((char)payload[i]);  
  }
  
  Serial.print(string);

  if (strcmp(topic, servo_topic) == 0)  
    Serial.print(" ");
    int resultado = string.toInt();   
    int pos = resultado;
    Serial.println(pos);
    myservo.write(pos);
    delay(300);
    myservo.write(0);
 }

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("ESP8266Client")) {
      Serial.println("connected");
      client.subscribe(servo_topic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}
void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
  delay(100);
}

In den markierten Zeilen noch die eigenen Daten eintragen SSID, WLAN-Passwort, MQTT-Broker und Topic und ab damit auf den WeMos. Kleine Anmerkung: Der Code ist quick and dirty zusammengeschustert. Hier fehlt noch z.B. WifiManager mit eigenen Parametern verwenden, Servo-Status publishen, Telemetrie-Daten des WeMos per MQTT publishen, LWT festlegen, Serial-Output mit debug-flag ein- und ausschaltbar machen usw. Ihr könnt gerne mithelfen 😉

Eine Sache im Code ist ganz wichtig: Bevor irgendeine Ausgabe auf der seriellen Schnittstelle erfolgt, muss das Servo initialisiert werden und auf seine 0-Stellung gesetzt werden. Wenn im Setup schon serial.print Befehle drin sind, dann spinnt das Servo. Wenn es dann in seiner Position ist, kann es sich auch mal weiter drehen, als notwendig, und dann ggf. doch die Taste evtl. zu lange drücken oder sich in die falsche Richtung drehen und mit dem Hebel am Steuergerät hängen bleiben. Was dann mit dem WeMos passiert? K.P.!

Für die Schaltung mache ich kein Fritzing auf. Mein Servo hat ein rotes, ein braunes und ein orangenes Kabel. Rot kommt am WeMos an 3,3V (oder 5V), braun an GND und orange an D4 (daher im code oben myservo.attach(2) – PIN2 ist D4 auf dem WeMos).

WeMos auf Breadboard im Multimedia-Feld im Schaltschrank

Das Servo wird nun an geeigneter Position auf dem Steuergerät befestigt:

ob es tatsächlich so schlau war, die grüne, dauerleuchtende Status LED des Steuergerätes mit dem Servo zu überkleben, werden wir in ein paar Wochen sehen…

Weiter geht es mit der zweiten Hürde: Absicherung der Lösung und Integration in FHEM.

Halten wir folgendes fest: FHEM ist aus dem Internet über https mit einem gültigen Zertifikat und einer definierten URL über DynDNS erreichbar und mit Benutzer und Passwort abgesichert, am besten noch mit einer eigenen FHEMWEB-Instanz. Stimmt’s? Wenn nicht: Sorry, aber das sind Basics, die müsst ihr euch anderweitig erarbeiten. Alles andere wäre jetzt grob fahrlässig und dann bitte die folgenden Schritte nicht nachmachen. Schreibt mir ggf. eine Mail und ich schicke euch Links dazu, die ich für meine FHEM-Installation genutzt habe, um diese Anforderungen zu erfüllen.

Die Überschrift sagt ja was von OTP und Siri-Shortcuts. Also kümmern wir uns zunächst mal um OTP: Ein „Einmalkennwort“.

https://de.wikipedia.org/wiki/Einmalkennwort

In FHEM gibt es ein GoogleAuth-Modul. Dieses kann man wie folgt anlegen:

defmod GoogleAuth GoogleAuth
attr GoogleAuth DbLogExclude .*
attr GoogleAuth ga_labelName FHEM

Weitere Infos dazu hier bei Matthias Kleine auf YouTube

Damit hat man dann eine zusätzlich Funktion in FHEM, um über eine Authenticator-App auf dem Smartphone ein OTP zu generieren und dieses in FHEM auf Gültigkeit zu prüfen.

Um jetzt FHEM „von Außen“ das OTP zu übergeben, legen wir mal ein Dummy-Device an:

defmod gAuthTest dummy
attr gAuthTest DbLogExclude .*
attr gAuthTest readingList AuthCode
attr gAuthTest setList AuthCode
attr gAuthTest stateFormat AuthCode

Damit geht ja schon mal folgendes:

https://meine.fhem.instanz/fhem?cmd=set%20gAuthTest%20AuthCode%20OTP_aus_der_App&XHR=1

… äh, nein! Stichwort csrfToken

Ich empfehle dazu folgenden Link https://wiki.fhem.de/wiki/CsrfToken-HowTo

Also alles etwas komplizierter, aber ihr seid ja Experten (sonst wäret ihr gar nicht hier).

Also, nachdem wir es nun erfolgreich geschafft haben über eine URL das Reading „AuthCode“ im Device „gAuthTest“ zu setzen, legen wir uns noch ein Notify an, welches überprüft, ob das ein gültiges OTP war:

defmod n_ValidateGAuth notify gAuthTest:AuthCode:.* {\
if (gAuth("GoogleAuth", ReadingsVal("gAuthTest", "AuthCode", "")) == 1) {\
			fhem("set DoorServo Value 25");;\
		}\
}
attr n_ValidateGAuth DbLogExclude .*
attr n_ValidateGAuth room 97_Logic

Hier habe ich jetzt etwas vorgegriffen. Der set-Befehl im Notify funktioniert natürlich noch nicht, weil das Device DoorServo nicht existiert. Also holen wir das schnell nach;

defmod DoorServo MQTT_DEVICE
attr DoorServo DbLogExclude .*
attr DoorServo IODev mqtt
attr DoorServo publishSet_Value /servo/cmnd
attr DoorServo room MQTT

Hier könntest Du jetzt einen Test machen, um alles in FHEM und auf dem WeMos programmierte zu prüfen, in dem Du einfach mal z.B. im Browser versuchst das Reading AuthCode im Device AuthTest zu setzen. Wenn Du das OTP korrekt in der URL eingegeben hast, dann sollte sich das Servo nun kurz um 25° bewegen und dann wieder auf die 0-Stellung zurückfallen. Wenn das OTP nicht passt, dann passiert einfach gar nix.(Und hier erwarte ich eine Menge Feedback, weil das so einfach nicht ist. Stichworte: URL mit BasicAuth aufrufen, URL codieren, csrf-Token korrekt angeben usw. Aber: Das muss verstanden werden und funktionieren, sonst geht’s hier nicht weiter. Fragen? Gerne per Mail.)

Also weiter gemäß der Überschrift im Beitrag. OTP ist also geklärt nun kommt Siri dran.

Ich meine, seit iOS 11 gibt es die Siri-Shortcuts und mittlerweile auch die „Kurzbefehle-App“ auf iPhones und iPads. Wer sich etwas mit den Apple Betriebssystemen auskennt, weiß vielleicht, dass es in Betriebssystemen von Apple immer eine Möglichkeit der IAC=“Inter Application Communication“ gegeben hat. Auf dem Mac gab es schon in den 90er’n AppleScript. Ich behaupte, dass viele Betriebe, die sich in den 90er’n hin zu Multimedia-Betrieben entwickelt haben, ohne AppleScript nicht überlebt hätten. Über AppleScript konnten Programme untereinander und mit dem Betriebssystem interagieren. Und das noch über eine „lesbare“ Programmiersprache. Die Weiterentwicklung war dann in Mac OS X der „Automator“ und als letztendliche Konsequenz kam dann diese Funktionalität auch mit den Siri-Shortcuts bzw. der Kurzbefehle-App auf Apple’s mobiles Betriebssystem iOS. (Sorry für den Exkurs, aber das war, ist und bleibt ein Grund für mich, Apple-Systeme zu nutzen. Ich stehe einfach auf IAC 😉 )

Und all das hilft uns jetzt, das „Problem“ zu lösen!

Wie wäre es, wenn man folgendes tun könnte:

  • „Hey Siri, Tür öffnen“
  • Siri antwortet mit „Gib den Zugangscode ein“
  • „123456“ zu Siri zu sprechen und die Tür geht auf

Und genau das machen wir jetzt abschließend zu dem Beitrag.

Anmerkung: Es ist leider nicht ganz so einfach, den Siri-Shortcut in Form von Text zu exportieren. Aufgrund der verwendeten Zugangsdaten zu FHEM werde ich diesen Kurzbefehl hier nicht zum Download anbieten. Daher werde ich Text und Bild verwenden, um euch den Kurzbefehl zu erklären.

Zunächst aktivieren wir in den Einstellungen des iOS Devices für die Kurzbefehle-App die Option „Nicht vertrauensw. erlauben“ (ob das wirklich notwendig ist, muss ich noch recherchieren, aber wir machen das jetzt einfach mal).

Wir starten mit dem Aufruf der Kurzbefehle-App, gehen auf „Meine Kurzbefehle“ und wählen „Kurzbefehl erstellen“ aus. Den Kurzbefehlnamen nennen wir „Tür öffnen“. Damit geht schon mal „Hey Siri, Tür öffnen“. Als erste Aktion fügen wir „Nach Eingabe Fragen“ ein und definieren die Frage „Gib den Zugangscode ein“. Eingabetyp ist „Text“ (weil das OTP auch mit einer Null beginnen kann).

Die Nächste Aktion ist „Variable konfigurieren“. Diese nennen wir „sOTP“ und setzen sie auf „Eingabe breitgestellt“ fest.

Die nächste Aktion ist „Text“. Hier geben wir die externe FHEM-URL ein und (leider) zwar mit der Angabe von Username und Passwort (Wenn jemand da eine andere Möglichkeit kennt: Her damit!) Also lautet die URL dann beispielsweise so:

https://username:passwort@meine.fhem.instanz

Als nächstes brauchen wir eine neue Aktion „zu Variable hinzufügen“ Und zwar fügen wir den Text zu sBaseURL hinzu.

Dann brauchen wir noch eine „Text“-Aktion. Diese enthält den FHEM-Befehl und wird ergänzt mit der Variable sOTP.

Dann brauchen wir eine URL Codieren Aktion (wie immer am besten die Suche verwenden, damit man diese Aktionen schnell finden kann). Mit dieser Aktion codieren wir den Text, damit Leerzeichen etc. entsprechend ersetzt werden.

Die dann folgende Aktion ist „Variable konfigurieren“. Hier legen wir die Variable sCMD auf den Text der codierten URL fest. Die Schritte sehen dann so aus:

So, jetzt kommt ein „tricky-part“. FHEM bzw. FHEMWEB hat seit einiger Zeut ja einen Schutzmechanismus gegen Cross-Site-Scripting-Attacks eingebaut. Das ganze wird über das csrf-Token geregelt. Und genau das holen wir uns jetzt mit den nächsten 3 Aktionen.

Die erste davon ist „Header von URL abrufen“. Hier bauen wir uns die URL wie folgt zusammen: sBaseURL/fhem?XHR=1

Die nächste Aktion ist „Wörterbuchwert abrufen“. Damit holen wir uns letztendlich aus einem named-array einen bestimmten Wert (Was für eine blöde Übersetzung). Und zwar den Wert für X-FHEM-csrfToken in Header von URL abrufen.

Die dritte Aktion, die wir jetzt brauchen ist wieder eine „Variable konfigurieren“ und zwar sCSRF auf Wörterbuchwert festlegen. Das sieht dann im Detail so aus:

wer findet den Schreibfehler?

Und zu guter Letzt rufen wir eine Aktion „Inhalte von URL abrufen auf“ und bauen darin die URL mit den einzelnen Variablen zusammen:

CSRF <> CSFR 😉

Fertig? Leider noch nicht so ganz. Beim Testen ist mir ein Eintrag im FHEM-Log aufgefallen, der wohl kein großes Problem verursacht, aber Einträge in Logfiles, die sich nicht so einfach erklären lassen, sind per se verdächtig. Es handelte sich um z.B. diese Zeile im log:

2020.05.22 20:46:50 3: WEB_192.168.xxx.xxx_12345: unsupported HTTP method HEAD, rejecting it.

Das konnte ich durch Setzen eines Attributes in der FHEMWEB-Instanz eliminieren.

attr WEB allowedHttpMethods GET|POST|HEAD

Fertig? Ja!