Eigenen Alexa Skill programieren und auf Raspi-Webserver hosten

In meinen letzten Projekten habe ich ja ein LC-Display mit vier Zeilen zu zwanzig Zeilen an einen Raspberry Pi Zero gelötet, eine Fernbedienungs-IR-LED eingebaut und das Ganze in ein ansprechendes, mit dem 3D-Drucker selbst designtes und gedrucktes Gehäuse gepackt.

Das Ganze ist dafür gedacht, auf meinem Schreibtisch zu stehen, so dass ich wichtige und interessante Informationen immer im Blick habe: den Status meines Web-Servers, Wetter, Börse, was immer ich will.

Kurzum: Das Always-On-Display steckt endlich im eigenen Gehäuse, ist per Infrarot-Fernbedienung steuerbar und zeigt auch brav alle Infos an.

Aber wäre es nicht eine feine Sache, wenn man das Display auch mit der Alexa auf dem amazon Echo Dot, den man sowieso schon hat, steuern könnte und wenn einem Alexa die Daten auch vorlesen würde, wenn man gerade nicht am Computer sitzt, sondern auf dem Sofa?


Das kann man schon haben. Doch vor das Genießen der Faulheit hat das Universum den Fleiß gestellt. Heute geht es darum, wie man einen eigenen Skill für Alexa schreibt und diesem auf dem eigenen Server hostet. Die AWS-Services wäre evtl. einfacher zu nutzen, aber das Kostenkonzept ist mir zu kompliziert, intransparent und birgt finanzielle Gefahren, die ich nicht einzugehen gedenke. Bevor ich mich da in die seitenlangen Geschäftsbedingungen und Tarife reinfuchse (und evtl. dann doch am Ende eine böse Überraschung erleben) schreibe und hoste ich mir das lieber selbst.

Als Webserver kann ja dann auch gleich der Raspi im Alway-On-Display herhalten - der hat eh fast nichts zu tun und Speicher ist doch auch noch genügend frei. Wichtig ist bei dem Homeserver-Konzept, so will ich das jetzt mal nennen, dass der Homeserver auch von außen erreichbar ist. Das heißt: er braucht unbedingt eine IPv4-Adresse, am besten eine feste oder man geht den Umweg, neue dynamische IP-Adressen, die dem eigenen Internetzugang zugewiesen werden, umgehend an einen Dynamic DNS-Server zu melden.

Da feste IP bei meinem Provider extra kostet, bin ich den Weg mit dem Melden von neuen dynamischen IP-Adressen gegangen. Und das kann der Raspberry Pi ja auch gleich mit erledigen. Natürlich ist außerdem für die Sicherheit des Raspi zu sorgen, damit es hier kein Einfallstor für Hacker ins eigene Netzwerk gibt. Aber das alles zu beschreiben, würde den Rahmen des Artikels sprengen und würde am eigentlichen Thema vorbei gehen.

Zu allererst gilt es aber, grob die Architektur der Alexa-Modells und der Skills zu verstehen. Ich habe das hier mal kurz skizziert:



Nein, ich erwarte jetzt nicht, dass ihr mein Gekrakel auf Anhieb versteht. Darum habe ich das hier Schritt für Schritt erklärt:



Der Ablauf ist also der Folgende: Wollen wir einen eigenen Skill, dann müssen wir: Der eigene Skill wird dann mit "Alexa, sage [Invocation name] [intent] [slots]" aufgerufen. Bzw. auf deutsch "Alexa, sage [Skillname] [Verb] [Parameter]", zum Beispiel: "Alexa sage Display Schalte Steckdose eins an". Hier ist "Display" der Skill, "Schalte Steckdose" der Intent und "eins" sowie "an" die Parameter.

Wie man einen eigenen Skill unter developer.amazon.com/alexa anlegt und diesen dann mit den entsprechenden Werten füllt, habe ich in diesem Video anhand meine Always-On-Displays beispielhaft erklärt:



Mein Display-Skill hat also bisher die Intents: Hallo und Wetter haben keine Parameter, Zeigen muss ein Parameter mitgegeben werden, nämlich die Screen-Nr., die gezeigt werden soll. Und Schalte hat zwei Parameter, die Steckdosennr. und ob an oder aus.

Am einfachsten und übersichtlichsten ist, die Intents so zu nennen, wie sie auch gesagt werden müssen und für die Parameter sprechende Namen zu wählen.

Mein Skill kann also z. B. auf folgende Alexa-Anweisungen antworten: Ist der Skill auf der amazon Developer Seite fertig gebuidelt, dann wird uns die Amazon Cloud jede Alexa-Anweisung für unseren Skill als JSON-Objekt an unserem Endpoint, also unserem angegeben PHP-Script abliefern.

Dies tut sie als Raw-Post-Data, die wir in PHP folgendermaßen erfassen können: [unser-skill.php] ... $rawdata = file_get_contents('php://input'); $file = fopen ("/home/pi/debug/php-raw-post.txt", "w"); fwrite ($file, $rawdata); fclose ($file); Der Inhalt sieht dann so aus: pi@raspberrypi:~/debug $ cat raw-post.txt {"version":"1.0","session":{"new":true,"sessionId":"amzn1.echo-api.session.970c186e-4c20-492d-a67b-856903ed0e7e","application":{"applicationId":"amzn1.ask.skill.xxx-xxx-xxx-xxx-xxx"},"user":{"userId":"amzn1.ask.account. yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"}},"context":{"System":{"application":{"applicationId":"amzn1.ask.skill.zzz-zzz-zzz-zzz-zzz"},"user":{"userId":"amzn1.ask.account.aaaaa aaaaa aaaaa aaaaa"},"device":{"deviceId":"amzn1.ask.device.bbbbb bbbbb bbbbb bbbbb bbbbb bbbbb bbbbb bbbbb","supportedInterfaces":{}},"apiEndpoint":"https:// api.eu.amazonalexa.com", "apiAccessToken":"ddd.ddddd-eeeeeeeeeeeeeeeeeeeee _ eeeeeeeeeeeeeeeeeeeeeeee-eeeee_eeeee"}},"request":{"type":"IntentRequest","requestId": "amzn1.echo-api.request.ffff-ffff-ffff-ffff-ffffffff", "timestamp":"2019-03-04T11:02:07Z","locale":"de-DE","intent":{"name":"hallo","confirmationStatus":"NONE"}}} ... amzn1.echo-api.request.xxxx","timestamp":"2019-03-04T10:50:48Z","locale":"de-DE","intent":{"name": "schalten","confirmationStatus": "NONE","slots":{"an_aus":{"name": "an_aus","value":"aus","resolutions": {"resolutionsPerAuthority": [{"authority": "amzn1.er-authority.echo-sdk.amzn1.ask.skill.xxxx.an_aus","status":{"code":"ER_SUCCESS_MATCH"},"values":[{"value":{"name":" aus","id":"xxxx"}}]}]},"confirmationStatus": "NONE","source":"USER"},"steckdose":{"name": "steckdose","value": "testgeraet","resolutions": {"resolutionsPerAuthority": [{"authority": "amzn1.er-authority.echo-sdk.amzn1.ask.skill. xxxx.steckdose","status":{"code":"ER_SUCCESS_MATCH"},"values":[{"value":{"name": "eins","id":" xxxx "}}]}]},"confirmationStatus": "NONE","source":"USER"}}}}} Nun können wir in $rawdata die Stellen suchen, die wir benötigen - in den beiden oberen Beispielen habe ich diese einmal gelb markiert.

Es gibt auch eine nützliche Dokumentationsseite mit den Übergabewerten. Der Link scheint sich allerdings häufiger zu ändern und mit Umleitungsseiten scheint es amazon auch nicht so genau zu nehmen. Wenn ihr also hier einen 404er bekommt, sucht nach dem Titel "Request and Response JSON Reference".

Die ganzen mitgegebenen IDs können wir gut dafür gebrauchen, zu schauen, ob der Aufruf berechtigt ist. Ich habe einfach alles, was nicht von meinem Alexa-Endgerät oder von meinem Amazon-Account kommt, geblockt. Aber da ich den Skill nicht gepublisht habe, sollte sowieso niemand den Skill verwenden können außer mir - aber sicher ist sicher.

Die oben gelb markierten Werte sind die eigentlich wichtigen. Hiermit entscheiden wir, was wie zu tun ist und basteln und damit einen Antworttext zusammen, den wir als Resonse auf den HTTPS-Request von amazon-Server zurückgeben. Er sieht im einfachsten Fall so aus: $out='{"version":"1.0","sessionAttributes":{"key":"value"},"response":{"outputSpeech":{"type":"PlainText","text":"'.$text.'", "playBehavior": "REPLACE_ENQUEUED"}, "shouldEndSession":true}}'; exit ($out); Wobei die Variable $text unser Anwort-Text ist, auch hier sollte man Umlaute als ae, oe, ue und ss schreiben, sonst kommt Alexa beim vorlesen ins Stolpern.

Wie das fertige System reagiert, habe ich mit ein paar zusätzlichen Erklärungen hier noch mal beispielhaft gezeigt:



Jetzt wo der Rahmen steht, wird es in Zukunft nicht schwierig sein, meinen Skill um weitere Intents und damit Funktionen zu erweitern. Der Anfang war zwar ein bisschen holprig, aber jetzt weiß man ja, wie der Hase läuft.