itemis Blog

Alexa, schalte ZDF ein

Geschrieben von Patrick Könemann | 12.04.2017

Wer von euch kennt das nicht? Ihr seht gerade fern und wollt umschalten, allerdings liegt die Fernbedienung mehrere Meter weit weg oder irgendwo unter Kissen begraben. Was ist also naheliegender als Alexa darum zu bitten, auch den Fernseher zu steuern? Vorausgesetzt, der Fernseher oder Receiver ist smart und über’s Netzwerk steuerbar.

Die erste Anlaufstelle ist der Skill Store, doch dort gibt es nur Skills, um das Fernsehprogramm zu erfragen. Die zweite Anlaufstelle ist Google, und da ergeben sich mehrere Möglichkeiten – via Skills die (noch) nicht auf Deutsch erhältlich sind und zusätzliche Hardware benötigen, z.B. ein Harmony Hub oder AnyMote. Allerdings muss bei AnyMote stets der Name des Skills mit genannt werden:

  • Alexa, tell AnyMote to Volume UP 5 times on my TV
  • Alexa, tell AnyMote to CHANNEL ONE HUNDRED AND TWENTY TWO

Der Harmony Hub Skill scheint etwas cleverer zu sein, er erlaubt zumindest folgende Phrasen:

  • Alexa, turn on Netflix/Hulu/HBO
  • Alexa, turn off the TV

Befehle zur Lautstärkeregelung versteht Alexa allerdings für das eigene Gerät (Echo/Dot), daher ist dafür die Nennung des Skills unumgänglich:

  • Alexa, tell Harmony to (raise/increase/lower/decrease) the volume
  • Alexa, tell Harmony to mute/unmute
  • Alexa, tell Harmony to pause/play

Um diese Skills nutzen zu können, müssten wir unseren Amazon-Account auf Englisch umstellen – was wir natürlich nicht wollen. Ansonsten bleibt uns aktuell nichts anderes übrig als zu warten, bis diese Skills auch auf Deutsch verfügbar sind – oder wir programmieren uns unseren eigenen Skill.

Smart und Custom als sinnvolle Skill-Kombi

Hierzu ist zunächst ein kleiner Exkurs notwendig, um zu verstehen, wie Alexa erweitert werden kann. Es gibt vier Kategorien, in die sich Alexas Fähigkeiten einteilen lassen:

  1. Built-in Fähigkeiten (keine expliziten Skills), die von Amazon kontrolliert und direkt implementiert werden. Zum Beispiel Wetterabfragen, Wissensfragen, Kalender- und ToDo-Listen-Zugriff, Easter Eggs, etc.
  2. Installierbare Nachrichten-Skills für die tägliche Zusammenfassung.
  3. Installierbare Smarthome-Skills um IoT-Geräte direkt zu steuern.
  4. Installierbare Custom-Skills um komplexere Dialoge, Abfragen oder Steuerungen durchzuführen.

Die Built-in-Fähigkeiten (1) sind vordefinierte Fragen und Phrasen, an denen wir nicht rütteln können. Nachrichten-Skills (2) bieten lediglich Inhalte an, die in der täglichen Zusammenfassung mit ausgegeben werden, bieten aber keinerlei Interaktion.

Smarthome-Skills (3) müssen sich an bestimmte Konventionen halten und erlauben es, Dinge ein oder aus zu schalten und Temperaturen zu regeln (in Zukunft werden sicherlich noch weitere Funktionen auf Deutsch hinzukommen). Custom-Skills (4) erlauben es mittels eines Keywords in ein sprachliches Submenü einzutauchen, in dem man vollständige Kontrolle über die gesprochenen Phrasen hat.

Letzteres hat einige Vor- aber auch Nachteile:

Vorteile

Nachteile

Beliebige Phrasen sind möglich.

Der Nutzer muss sich das Keyword merken, um den Custom-Skill zu verwenden.

Reservierte Phrasen sind möglich, wie z.B. Lauter, Ton aus, Was gibt’s Neues?

Innerhalb des Skills können (bis auf einige Ausnahmen) keine anderen Fähigkeiten verwendet werden.

Komplexe Dialoge inkl. Rückfragen sind möglich.

Die Phrasen inkl. Keyword können sehr lang werden; Beispiel:
“Alexa, sage Magenta Smarthome, die Rollladen im Wohnzimmer auf fünfzig Prozent zu stellen”.

Ein Custom-Skill ist zustandsbasiert, d.h. er kann sich Dinge "merken".

Um beliebige Phrasen korrekt zu erkennen, ist eine umfangreiche Spezifikation der Interaktion mit dem Custom Skills erforderlich (betrifft die Skill-Entwickler).

Die Phrasen für unsere Fernsehersteuerung sollen so einfach und intuitiv wie möglich sein. Unser Ziel ist es, Kanäle zu wechseln und die Lautstärke zu regulieren. Jetzt können wir natürlich alle Funktionen als Custom-Skill implementieren (was immer das Skill-Keyword erfordert). Setzen wir das Keyword auf Fernseher, sollte folgendes möglich sein:

  • Alexa, sage Fernseher lautlos/Ton aus/mute
  • Alexa, sage Fernseher Lautstärke 10
  • Alexa, sage Fernseher Kanal rauf/hoch/runter

Zusätzlich können wir aber auch die Smarthome-Phrasen missbrauchen, um Kanäle in Form von virtuellen Geräten zu schalten:

  • Alexa, schalte ARD/ZDF/... ein
  • Alexa, schalte Fernseher aus

Ab in die Cloud!

Die o.g. Skills für Harmony Hub oder AnyMote sind Out-of-the-Box-Lösungen, erfordern besagte Geräte – und sind noch nicht auf Deutsch erhältlich. Für eine eigene Lösung wird, wie für jeden Skill übrigens, auch für die beiden gerade genannten, ein Server in der Cloud benötigt.

Wir führen das Ganze für openHAB mittels des Cloud-Service myopenhab durch, das sollte jedoch auch auf andere Dienste adaptiert werden können. Voraussetzung ist, dass der zu steuernde Fernseher (oder Receiver) in openHAB über Items zur Kanalwahl und Lautstärkeregelung konfiguriert ist (z.B. via Bindings LG Netcast, LG WebOs, Panasonic, Philips, oder Samsung).

Falls es für euren Fernseher oder Receiver kein solches Binding gibt, die Steuerung übers Netzwerk aber z.B. durch einen Konsolenbefehl oder Script möglich ist, könnt ihr immer noch das exec-Binding zur Steuerung verwenden.

Ein Teil unseres Vorhabens kann nun bereits automatisiert durchgeführt werden, denn es gibt glücklicherweise schon einen Smarthome-Skill für openhab. Die Sourcen sind in diesem git-Repository verfügbar, wo auch erklärt wird, wie Geräte in openHAB konfiguriert werden müssen, damit sie von Alexa gefunden werden.

In unseren Fall benötigen wir drei Items für Lautstärke und Senderwahl, und einige virtuelle Items (die nicht direkt ein Gerät repräsentieren) für den Smarthome-Skill:

Switch TV_Power "Fernseher" ["Lighting"] { channel="..." } // used to switch TV on and off
String TV_Channel { channel="..." } // used to switch TV channels
Number TV_Volume { channel="..." } // used to set TV volume
Switch TV_Mute { channel="..." } // used to (un)mute TV
Switch TV_Channel_ARD "ARD" ["Lighting"] // virtual item to allow Alexa to directly switch to a channel
Switch TV_Channel_ZDF "ZDF" ["Lighting"] // virtual item to allow Alexa to directly switch to a channel
Switch TV_Channel_WDR "WDR" ["Lighting"] // virtual item to allow Alexa to directly switch to a channel
...

Fangen wir mit dem Smarthome-Skill an:
Mittels Tags (in eckigen Klammern) wird ein Switch-Item als schaltbares Gerät gekennzeichnet (“Lighting”, angelehnt an die Homekit-Notation: ‘A lightbulb, switchable, dimmable, or rgb’) und kann deshalb von Alexa erkannt werden.

Ein Satz von Regeln reagiert darauf, sobald ein virtuelles Item von Alexa geschaltet wird und führt dann die eigentliche Senderwahl durch (der tatsächliche Command hängt vom verwendeten Binding ab):

rule "tv ard" when Item ARD received command then TV_Channel.sendCommand("1") end
rule "tv zdf" when Item ZDF received command then TV_Channel.sendCommand("2") end
rule "tv wdr" when Item WDR received command then TV_Channel.sendCommand("15") end
...

Lässt man nun Alexa seine smarten Geräte erkennen, sollten alle definierten Sender gefunden und in der App als Geräte gelistet werden:

  • Alexa, erkenne Geräte.
  • Suche abgeschlossen. Ich habe 8 Smarthome Geräte gefunden..
  • Alexa, schalte ARD ein.
  • OK.
  • Alexa, schalte ZDF ein.
  • OK.


Sehr schön, die Senderwahl ist mittels überaus einfacher Phrasen realisiert.

Wir bauen uns einen Custom-Skill

Etwas umfangreicher ist die Definition des Custom-Skills zur Lautstärkeregelung, denn dafür gibt es noch keinen vordefinierten Skill. Daher nutzen wir das Alexa Skill Kit (ASK) und eine eigene Lambda-Funktion in den Amazon Web Services (AWS), um unser Vorhaben zu realisieren.

Als erstes erstellen wir die Funktion zum Setzen der openHAB-Items via myopenhab:

  1. Bei aws.amazon.com (kostenlos) anmelden.
  2. In der AWS Management Console rechts oben die Region auf ‘EU (Ireland)’ umschalten.
  3. Links oben unter ‘Services’ – ‘Lambda’ – ‘Functions’ – ‘Create a Lambda Function’ – ‘Blank Function’ – als Trigger: ‘Alexa Skills Kit’.
  4. Name: ‘Fernseher’, Runtime: ‘Node.js 4.3’, Role: ‘Create new role from template(s)’ (‘lambda_basic_execution’).
  5. Copy & replace JavaScript Code von hier in den Editor im Browser und setze USER und PASSWORD (keine Bange, der Code ist ausschließlich in eurem privaten Amazon Account gespeichert und kann von niemand anderem eingesehen werden):

    /*
    * The purpose of this code is merely to demonstrate how to control your TV
    * volume via https://myopenhab.org with an Alexa custom skill. Adjust the
    * constants at the top to your setup. Please note that the code is a prototype
    * and is neither thoroughly tested, nor does it do proper error handling.
    * The code is based on the ‘alexa-skills-kit-color-expert’ template.
    */
    'use strict';
    const USER = "max@mustermann.de"; // email address for myopenhab.org const PASSWORD = "mustermann123"; // password for myopenhab.org const ITEM_POWER = "TV_Power"; // openhab item for TV power const ITEM_VOLUME = "TV_Volume"; // openhab item for TV volume const ITEM_MUTE = "TV_Mute"; // openhab item for muting TV var https = require('https'); // --------------- Helpers that build all of the responses ----------------------- function buildSpeechletResponse(spokenText, shouldEndSession) { return { // no image and no card, just voice outputSpeech: { type: 'PlainText', text: spokenText }, shouldEndSession }; } function buildResponse(sessionAttributes, speechletResponse) { return { version: '1.0', sessionAttributes, response: speechletResponse }; } // --------------- Functions that control the skill's behavior ----------------------- function setTVCommand(callback, item, value) { const options = { hostname: "myopenhab.org", port: 443, path: "/rest/items/" + item + "/state", method: "PUT", auth: USER + ":" + PASSWORD, headers: {} }; options.headers['Content-Type'] = 'text/plain'; options.headers['Content-Length'] = value.length; // console.log("request options: " + JSON.stringify(options)); var req = https.request(options, function (response) { var body = ''; if (response.statusCode === 200 || response.statusCode === 201 || response.statusCode === 202) { callback({}, buildSpeechletResponse('OK', true)); } else { console.log("Fehler in https request (" + response.statusCode + "): ", response); callback({}, buildSpeechletResponse('Unerwarteter Return Code: ' + response.statusCode, true)); } response.on("error", function (e) { console.log("request error: " + JSON.stringify(e)); callback({}, buildSpeechletResponse('Unerwarteter Fehler: ' + e.message, true)); }); }); req.write(value); req.end(); } // --------------- Events ----------------------- /** * Called when the session starts. */ function onSessionStarted(sessionStartedRequest, session) { console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`); } /** * Called when the user launches the skill without specifying what they want. */ function onLaunch(launchRequest, session, callback) { console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`); getWelcomeResponse(callback); } /** * Called when the user ends the session. * Is not called when the skill returns shouldEndSession=true. */ function onSessionEnded(sessionEndedRequest, session) { console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}`); } function getWelcomeResponse(callback) { // If we wanted to initialize the session to have some attributes we could add those here. const sessionAttributes = {}; const msg = 'Du kannst die Fernseher Lautstärke verändern.'; callback(sessionAttributes, buildSpeechletResponse(msg, false)); } function handleSessionEndRequest(callback) { callback({}, buildSpeechletResponse('Fernseher Steuerung beendet.', true)); } /** * Called when the user specifies an intent for this skill. */ function onIntent(intentRequest, session, callback) { console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`); const intent = intentRequest.intent; const intentName = intentRequest.intent.name; // Dispatch to your skill's intent handlers if (intentName === 'Mute') { setTVCommand(callback, ITEM_MUTE, 'ON'); } else if (intentName === 'Ton') { setTVCommand(callback, ITEM_MUTE, 'OFF'); } else if (intentName === 'Aus') { setTVCommand(callback, ITEM_POWER, 'OFF'); } else if (intentName === 'An') { setTVCommand(callback, ITEM_POWER, 'ON'); } else if (intentName === 'Volume') { const VolumeSlot = intent.slots.value; if (VolumeSlot && VolumeSlot.value === parseInt(VolumeSlot.value, 10).toString()) { setTVCommand(callback, ITEM_VOLUME, VolumeSlot.value.toString()); } else { var msg = "Ich konnte die Lautstärke '" + VolumeSlot.value + "' nicht verarbeiten."; callback({}, buildSpeechletResponse(msg, true)); } } else if (intentName === 'AMAZON.HelpIntent') { getWelcomeResponse(callback); } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') { handleSessionEndRequest(callback); } else { throw new Error('Invalid intent'); } } // --------------- Main handler ----------------------- // Route the incoming request based on type (LaunchRequest, IntentRequest, // etc.) The JSON body of the request is provided in the event parameter. exports.handler = (event, context, callback) => { try { console.log(`event.session.application.applicationId=${event.session.application.applicationId}`); /** * Uncomment this if statement and populate with your skill's application ID to * prevent someone else from configuring a skill that sends requests to this function. */ // if (event.session.application.applicationId !== 'amzn1.ask.skill.816d3b56-abcd-1234-98b2-1e544500a25a') { // callback('Invalid Application ID'); // } if (event.session.new) { onSessionStarted({ requestId: event.request.requestId }, event.session); } if (event.request.type === 'LaunchRequest') { onLaunch(event.request, event.session, (sessionAttributes, speechletResponse) => { callback(null, buildResponse(sessionAttributes, speechletResponse)); }); } else if (event.request.type === 'IntentRequest') { onIntent(event.request, event.session, (sessionAttributes, speechletResponse) => { callback(null, buildResponse(sessionAttributes, speechletResponse)); }); } else if (event.request.type === 'SessionEndedRequest') { onSessionEnded(event.request, event.session); callback(); } } catch (err) { callback(err); } };
  6. Actions – Configure test event – füge dieses Snippet ein – Save and Test – unten eine Datenstruktur mit “text”: “OK” angezeigt werden:
    {
     "session": {
       "sessionId": "SessionId.a7c6ea36-5d4d-40c7-936c-dba524758bbd",
       "application": {
         "applicationId": "amzn1.ask.skill.3ddb9fb0-eeee-4986-ffff-03dd0093edfb"
       },
       "attributes": {},
       "user": {
         "userId": "amzn1.ask.account.ABCXYZ"
       },
       "new": true
     },
     "request": {
       "type": "IntentRequest",
       "requestId": "EdwRequestId.b9e1ad2a-aaaa-49f3-cccc-0cbf58c7684a",
       "locale": "de-DE",
       "timestamp": "2017-03-22T11:46:51Z",
       "intent": {
         "name": "Volume",
         "slots": {
           "value": {
             "name": "value",
             "value": "20"
           }
         }
       }
     },
     "version": "1.0"
    }

Der openHAB-spezifische Code ist in der Funktion setTVCommand enthalten und besteht aus einem https-Request auf der REST-API von myopenhab.org, um die Zustände einiger Items in openHAB zu setzen. Für eine Fernsehsteuerung über einen anderen Dienst muss dieser Code natürlich abgeändert werden. Damit haben wir unseren Glue-Code zwischen einem Alexa-Custom-Skill und myopenhab fertig.

Als nächstes definieren wir den eigentlichen Custom-Skill:

  1. Bei developer.amazon.com (kostenlos) anmelden (mit dem gleichen Amazon Account wie Alexa!).
  2. Gehe zu ‘Alexa’ - ‘Alexa Skills Kit’ - ‘Get Started >’ - ‘Add a New Skill’.
  3. Setze Skill Typ ‘Custom Interaction Model’, Sprache ‘German’, Name & Invocation Name ‘Fernseher’.
  4. Füge dieses Intent Schema ein:
    {
     "intents": [
       { "intent": "Mute", "slots": [] },
       { "intent": "Ton", "slots": [] },
       { "intent": "An", "slots": [] },
       { "intent": "Aus", "slots": [] },
       { "intent": "Lauter", "slots": [] },
       { "intent": "Leiser", "slots": [] },
       { "intent": "Pause", "slots": [] },
       { "intent": "Play", "slots": [] },
       { "intent": "Volume", "slots": [ { "name": "value", "type": "AMAZON.NUMBER" } ] }
     ]
    }
  5. Und füge diese Sample Utterances ein:
    Mute lautlos
    Mute still
    Mute Ton aus
    Mute Ton aus schalten
    Mute Ton aus stellen
    Ton Ton an
    Ton Ton an schalten
    Ton Ton an stellen
    Ton Ton einschalten
    Ton Ton einstellen
    An an
    An an schalten
    An an stellen
    An einschalten
    An einstellen
    Aus aus
    Aus aus schalten
    Aus aus stellen
    Volume Lautstärke {value}
  6. Als Endpoint ‘AWS Lambda’ und ‘Europe’ auswählen, und die bei der oben erstellten Lambda Function rechts oben angezeigte ID eintragen (beginnt mit ‘arn:aws:lambda:eu-west-1:’).
  7. Dann im Tab ‘Test’ im Service Simulator als ‘Utterance’ probehalber eingeben: ‘Ton aus’ - dann sollte unten eine Datenstruktur mit “text”: “OK”  angezeigt werden.

In der Alexa-App sollte automatisch unser neuer Fernseher-Skill gelistet sein (da wir außer dem Namen noch keine weiteren Informationen zum Skill angegeben haben, fehlen Icon und Beschreibung; ‘devDE’ bedeutet, dass der Skill nicht öffentlich ist):

Nach diesen Mühen können wir nun endlich faul auf der Couch liegen:

  • Alexa, schalte ZDF an.
  • Alexa, sage Fernseher Ton aus/an.
  • Alexa, sage Fernseher Lautstärke 10

Ganz schön umständlich?

Ja und nein. Durch Alexas Einschränkungen bei Smarthome-Skills und durch die vorgegebene Grammatik für Custom-Skills ist es schwierig, einen Mix aus vernünftigen Phrasen für einen Anwendungsfall zu finden. Da würde ich mir ab und zu mehr Freiheiten wünschen, z.B. eine freiere Definition von Phrasen ohne Skill Keyword – was aktuell nur bei Built-in-Funktionen und Smarthome-Skills möglich ist.

Dadurch ergeben sich für Custom-Skills (das sind ja die meisten aus dem Skill-Store) unnötig kompliziert erscheinende, technisch aber nachvollziehbare Phrasen.
Diese Phrasen-Komplexität zusammen mit der noch jungen ungeübten deutschen Spracherkennung ergibt ein aktuell ernüchterndes Erlebnis, sofern man nur durch die vorhandenen Skills stöbert.

Sobald man sich aber die Mühe macht, für sich selbst sinnvolle und angepasste Funktionen zu ergänzen, gewinnt Alexa stark an Komfort :-) Bisher gab es keine wirklichen Alternativen zu Alexa, wenn es darum geht eigene Geräte mit der Sprache zu steuern. Apples Siri kann um einige spezielle Features erweitert werden, aber Geräte lassen sich damit nicht schalten.

Samsungs neues Bixby scheint vorerst ein geschlossenes System zu werden. Lediglich der vor kurzem erschienene Google Assistant via Google Home scheint mittels Actions ebenfalls frei erweiterbar zu werden – zumindest sieht dessen Entwickler-Doku sehr vielversprechend aus.

Nun gilt es noch herauszufinden, ob und wie eigene Anwendungsfälle wie das Steuern des eigenen Fernsehers damit realisiert werden können.

Fazit

Hat man die Funktionsweise von Alexa erst einmal verstanden, dann lassen sich damit schon ganz beachtliche Dinge realisieren – und das bequeme Steuern des TVs kann sogar den WAF von Alexa erhöhen ;-) Die Kombination aus Smarthome- und Custom-Skill erfordert zwar die Installation (bzw. ggf. auch die Implementierung) zweier Skills für eine Funktionalität, aber dafür bekommt man komfortabel kurze Phrasen, soweit es technisch möglich ist. Und wenn man das einmal geschafft hat, ist die Lernkurve beschritten und man bekommt schnell eine Vorstellung davon und auch Ideen darüber, was noch alles möglich ist.

Dieser Blog-Beitrag ist nach den ersten Erfahrungen mit Alexa der zweite einer Serie, die von unseren Experimenten mit Alexa berichtet. Weitere, auch (software-)technische Beiträge werden folgen – zum Beispiel das Experiment, bestimmte Personen in einem Gebäude zu lokalisieren.