Ziele

Es soll Amazons Alexa-Dienst (via Echo Dot) benutzt werden, um einen Husqvarna Automower 420 mit Connect-Interface mit Sprache zu steuern. Die Steuerfunktion wird mit NodeJs erstellt. Außerdem wird eine private Smart Home Steuerung per TCP und eigenem Protokoll mit Statusinformationen versorgt.

Es ist sinnvoll, parallel die Quelle für die Skill-Dokumentation bereit zu halten.

Inhalt

Dieser Beitrag zeigt kurz, wie man das Smarthome Skill für ALexa programmiert. Benutzt wird dazu eine AWS Lambda-Funktion, die die Husqvarna-Server benutzt, um mit dem Mähroboter zu kommunizieren.

Der Smarthome Skill ist ein vordefinierter, anprogrammierbarer Skill, der für Begriffe aus dem Haushaltsbereich optimiert ist. Er benötigt — und das ist der eigentliche Vorteil — kein Aktivierungswort. Normale Skills werden über ein Schlüsselwort aktiviert, z.B. „Frage ‚Fußball‘ nach den neuesten Ergebnissen‘. ‚Fußball‘ ist hier die Aktivierung, den Rest erledigen dann die hinterlegten Ausprägungen (uterances) im Skill. Beim Smarthome ist es anders. Hier wird auf Begriffe wie ‚ein‘, ‚einschalten‘ usw. reagiert. „Alexa, schalte Wohnzimmerlicht ein“ wäre so eine Aufforderung, die problemlos funktioniert. Allerdings ist der Wortschaft etwas beschränkt, so exotische Wörter wie „Mähroboter“ werden praktisch nicht erkannt.

Arbeitsschritte

Folgende Dinge werden benötigt:

  1. Ein Echo-Dot (oder anderes Echo-Gerät) mit Alexa
  2. Ein Husqvarna Mähroboter mit Connect-Interface (via GPRS/EDGE)
  3. Eine AWS-Lambda-Funktion mit Authentifizierung (via IAM)

Die Alexa-Funktion wird benutzt, um die Echo-Dots zu konfigurieren. Dies passiert mit der App oder auf der Website.

Zum Mähroboter gibt es das Connect-Modul einzeln bei Husqvarna. Es wird mit einem 2-Jahres-Vertrag für die SIM-Karte geliefert, lässt sich also sofort und ohne Zusatzkosten in Betrieb nehmen. Ob Husqvarna dies dauerhaft kostenlos anbietet, ist derzeit unklar. Notfalls lässt sich die SIM-Karte gegen eine eigene austauschen. Der Datenverkehr liegt bei wenigen MB pro Monat, sodass der allerkleinste Vertrag ausreichend ist.

Die Lambda-Funktion wird bei AWS (Amazon Web Services) eingerichtet. Hierfür kann dasselbe Amazon-Konto benutzt werden, dass auch für Alexa genommen wird. Ohne Konto geht beides nicht.

Kosten

Die Cloud-Funktionen von AWS haben meist geringe Frei-Kontingente. Wer nur gelegentlich damit Hausfunktionen steuert, wird kaum über diese Grenzen kommen. Nach 6 Monaten Roboter-Steuerung waren im Test keine Kosten aufgelaufen. Etwas anderes ist es, wenn Speicherfunktionen, wie bei der DynamoDb intensiv genutzt werden. Aber auch hier waren die Beträge mit 1 bis 4 Euro pro Monat gering. In jedem Fall gilt aber, dass eine gültige Kreditkarte hinterlegt werden muss. Die Kosten lassen sich deckeln oder per Alarm überwachen, sodass es keine Überraschungen geben kann.

Die Husqvarna-Endpunkte

Zuerst müssen die Dienstendpunkte bei Husqvarna bekannt sein. Dazu gehört auch ein Konto, dass man sich bei Husqvarna anlegen muss. Am besten geht es über die App (für Android oder iOS).

Benutzername und Kennwort werden auch für die API bei Husqvarna benutzt. Für die Lambda-Funktion werden zuerst Konstanten definiert (hier in JavaScript), die die Endpunkte und Zugänge definieren:

const MOWER_USER = 'meine.mail@husyqvarna.konto';
const MOWER_PW = 'mein-geheimes-kennwort-bei-husqvarna';
const AUTHURL = 'iam-api.dss.husqvarnagroup.net';
const TRACKURL = 'amc-api.dss.husqvarnagroup.net';
const AUTHURL_PATH = '/api/v3/';
const TRACKURL_PATH = '/v1/';

Einrichten der Lambda-Funktion

Um Alexa in West-Europa nutzen zu können, muss das Rechenzentrum EU-WEST-1 benutzt werden. Das steht in Irland. Das AWS-Center in Frankfurt unterstützt die Skill-Funktionen für Alexa derzeit nicht. Kritisch ist das nicht, die Laufzeiten sind durchgehend akzeptabel. Der Start beginnt also hier:

Nach dem Anmelden an der Konsole wird aus den vielen Funktionen „Lambda“ gewählt:

Prüfen Sie hier nochmals, dass das richtige Center ausgewählt ist.

Wählen Sie dort Create Function:

Es erfolgt nun die Wahl einer Vorlage. Für Smarthome-Skills gibt es eine fertige Vorlage:

Klicken Sie auf den Namen der Vorlage (das sind Links), nicht auf Author from scratch.

Sie gelangen nun in die Konfiguration der Funktion.

Sie können hier gleich eine neue IAM-Rolle anlegen — wenn bereits eine angelegt wurde, z.B. bei früheren Experimenten, dann kann man die hier auch auswählen. Die Rolle sollte das Recht haben, Funktionen auszuführen.

Die Lambda-Funktion muss nun mit dem Smarthome-Skill von Alexa verbunden werden. Dies umfasst zwei Schritte. Zum einen eine authentifizierte Verbindung. So wird sichergestellt, dass nur Alexa auf die Funktion zugreifen kann und niemand sonst. Zum anderen wird Alexa lernen, auf die Funktion zuzugreifen (im ersten Schritt zum Erkunden der Geräte == Discovery).

Alexa hat dafür eine Application-Id, die über den Entwickler-Stack von Alexa abgerufen werden kann. Der ist unter der Adresse https://developer.amazon.com/edw/home.html#/ zu finden. Wählen Sie dort die Funktion SkillKit:

Dort erstellen Sie ein neues Skill:

Auf der nächsten Seite wird der Typ und Name eingestellt:

Wichtig ist hier, dass Smart Home Skill API benutzt wird (und natürlich Deutsch als Sprache). Nach den Speichern wird nun die ApplicationId angezeigt…

…die im gleichnamigen Feld bei Lambda eingetragen werden muss.

Jetzt wird die Sache etwas komplexer. Um das Skill fertigzustellen, muss der Endpunkt der Lambda-Funktion bekannt sein. Der Ablauf unten zeigt, wie der Code dort aussieht. Wenn das fertig ist, dann steht der Endpunkt bereit. In der Lambda-Funktion finden Sie die ARN rechts oben:

Dieser Wert ist nun wiederum dem Skill auf der Alexa-Seite mitzuteilen.

Neben der reinen Verbindung mit ARN und ApplicationId muss das Smarthome-Skill auch authentifiziert werden. Die Rolle für Lambda ist nur eine Ausführ-Genehmigung. Sie eignet sich nicht, um Alexa des Zugriff zu erlauben. Aus Sicht von Lambda wird Alexa als „fremd“ angesehen und der Zugriff muss explizit geschützt werden. Dazu dient das Authentifizierungsverfahren OAUTH2. Glücklicherweise hat AWS auch einen OAUTH2-Dienst, der das gleich mit erledigen kann. Die dort ermittelten Daten werden dann im Konfigurationsdialog des Skill hinterlegt:

OAUTH 2 mit AWS

Client Id und Client Secret sind hier die kritischen Teile. Der Einstieg in die Hilfe informiert über die wichtigsten Begriffe. Im Grund läuft es darauf hinaus, dass sich eine Person (oder implizit die App) anmeldet und dann daraus ein Token erzeugt wird, der auf der Gegenseite akzeptiert wird. Es ist nun so, dass Alexa bei Amazon läuft, während Lambda bei AWS liegt (im Grund derselbe Konzern, aber technisch getrennte Systeme). Das Konto, das hier benötigt wird, wird also nicht AWS sondern bei Amazon erstellt.

Gehen Sie nun zum Entwicklerportal und wählen Sie dort die Funktion Login with Amazon:

  • Startseite: https://developer.amazon.com/myapps.html
  • Login with Amazon: https://developer.amazon.com/lwa/sp/overview.html

Die Idee ist ja, dass solche Skills jeder benutzen kann. Folglich muss ein Anmeldeverfahren her. Da jeder Alexa-Benutzer zwangsläufig ein Amazon-Konto hat, ist dies der Weg.

An dieser Stelle sollte klar erwähnt werden, dass das Skill am Ende NICHT veröffentlicht wird. Es bleibt privat und erscheint nur im eigene Alexa-Konto. Natürlich könnte man es veröffentlichen, aber dann müsste das Kennwort und Benutzername für den Zugriff auf Husqvarna (und damit letztlich auf den Mäher) irgendwo abgelegt werden (also ein Match von [Amazon-Konto == Husvarna-Konto]. Dazu müsste man weitere Dienste wie die DynamoDb einsetzen. Das wäre aber eher ein Projekt für Husqvarna 🙂

Der nächste Schritt ist das Anlegen eines Profils Create a new Security Profile:

Nun wird ein Name und ein Beschreibungstext vergeben. Die Policy-URL ist ein Pflichtfeld, auch wenn dies für einen privaten Skill nicht notwendig ist. Ich habe mal die von Amazon hier genommen.

Nun können auf der Übersichtsseite die beiden Informationen abgerufen werden, die im Skill noch fehlen:

Test

Jetzt lässt sich auf der Testseite des Skills die Testfunktion einschalten. Wenn der nachfolgend gezeigte Code in der Lambda-Funktion funktioniert, kann Alexa den Mäher sehen und steuern.

Wir bereits erwähnt wird das Skill jetzt NICHT veröffentlicht. Wir lassen es einfach im Testmodus laufen und haben ein eigenes, privates, sicheres und ziemlich cooles Skill für den Mäher. Alle Funktionen, Erweiterungen, Anpassungen etc. finden nun nur über die Lambda-Funktion statt. Dazu mehr in den folgenden Abschnitten.

Alexa Discovery

Die Lambda-Funktion hat zwei wichtige Aufgaben:

  • Discovery
  • Handler

Die erste dient dazu, Alexa in die Lage zu versetzen, die Geräte zu erkennen die steuerbar sind. Die zweite reagiert dann auf den entsprechenden Befehl. Um später mehr Funktionen benutzen zu können, ist es sinnvoll, die möglichen Befehle gleich in ein Array zulegen:

const mappingApiCommands = [
  {
    "applianceId": "api_r2d2",
    "friendlyName": "Robbi"
  }
];

Dabei ist applianceId eine eindeutige ID, die Discovery mit dem Handler verbindet. Beim Abruf wird die Funktion verlassen — der Aufruf kommt von außen — und dann wieder betreten, wenn Alexa die Aufgabe verstanden hat. Die applianceId dient der Erkennung des Befehls. Der friendlyName ist der Begriff, auf den Alexa reagiert. Der Wortschatz ist überschaubar. Zumindest Mitte 2017 waren Kombinationen mit „Mäh-“ nicht möglich. Allerdings kann man sich später in der Alexa-App noch Alias-Namen mit der Gruppenfunktion bauen, ohne erneut Änderungen am Discovery vorzunehmen.

Der Einstieg in die Lambda-Funktion sind standardmäßig so aus (der Name handler ist änderbar):

exports.handler = (event, context) => {
  console.log(`event=${event}`);
  var result;
  if (event.header && event.header.namespace) {
    switch (event.header.namespace) {

      case 'Alexa.ConnectedHome.Discovery':
        handleDiscovery(event, context);
        break;
      case 'Alexa.ConnectedHome.Control':
        result = handleControl(event, context);
        break;
      default:
        console.log('Err', 'No supported namespace: ' + event.header.namespace);
        context.fail('Something went wrong');
        break;
    }
  }
  console.log("After Handler");
  if (result) {
    if (result.payload.manufacturer === "Husqvarna") {
      console.log("Has payload: " + result.payload.applianceId + " - " + result.payload.v);
      sendHusqvarna(result.payload.applianceId, result.payload.v, (data) => {
        context.succeed(result);
      });
    }
  } else {
    console.log('Discovery done, no results');
  }
};

Den namespace gibt AWS vor. Für die Erkundung von Geräten ist dies ‚Alexa.ConnectedHome.Discovery‘. Für das Ausführen von Befehlen ‚Alexa.ConnectedHome.Control‘. Diese Liste kann durchaus erweitert werden, wenn Alexa mehr Funktionen lernt. Es ist also sinnvoll, hier mit switch statt if zu arbeiten. sendHusqvarna wird weiter unten genauer erklärt.

Geräte erkunden

Die Abbildung der Funktionen handleDiscovery und handleControl ist nun die nächste Aufgabe.

function handleDiscovery(event, context) {

  /**
   * Crafting the response header
   */
  var headers = {
    namespace: event.header.namespace,
    name: 'DiscoverAppliancesResponse',
    payloadVersion: event.header.payloadVersion,
    messageId: event.header.messageId
  };

  /**
   * Response body will be an array of discovered devices.
   */
  var appliances = [];
  // Devices
  for (let map in mappingApiCommands) {
    let device = {
      'applianceId': mappingApiCommands[map].applianceId,
      'manufacturerName': 'Husqvarna',
      'modelName': 'Mower',
      'version': '1',
      'friendlyName': mappingApiCommands[map].friendlyName,
      'friendlyDescription': 'Das Gerät heißt ' + mappingApiCommands[map].friendlyName,
      'isReachable': true,
      'actions': [
        "turnOn",
        "turnOff"
      ],
      'additionalApplianceDetails': {
        'manufacturerName': 'Husqvarna'
      }
    };
    appliances.push(device);
  }

  /**
   * Craft the final response back to Alexa Smart Home Skill. This will include all the 
   * discoverd appliances.
   */
  var payloads = {
    discoveredAppliances: appliances
  };
  var result = {
    header: headers,
    payload: payloads
  };

  console.log('Discovery count: ' + result.payload.discoveredAppliances.length);

  context.succeed(result);
}

Die Übergabe der Daten erfolgt mit einer JSON-Struktur über context.succeed.

Im Grunde wird nur das Array ausgelesen und in die geforderte Struktur verpackt. Dies sieht so aus:

{
  header: {
    namespace: 'Alexa.ConnectedHome.Discovery',
    name: 'DiscoverAppliancesResponse',
    payloadVersion: '',
    messageId: '' 
  },
  payload: {
    discoveredAppliances: [
    ]
  }
}

Das Feld discoveredAppliances ist ein Array mit allen Geräten. Jedes Element des Arrays hat folgende Struktur:

{
   'applianceId': '',
   'manufacturerName': 'Husqvarna',
   'modelName': 'Mower',
   'version': '1',
   'friendlyName': '',
   'friendlyDescription': '',
   'isReachable': true,
   'actions': [
       "turnOn",
       "turnOff"
   ],
   'additionalApplianceDetails': {
      'manufacturerName': 'Husqvarna'
   }
}

Wichtig sind:

  • applianceId zum Wiederkennen in der Funktion
  • friendlyName zur Spracherkennung — auf dieses Wort „hört“ Alexa
  • friendlyDescription für die Anzeige in der Alexa-App
  • isReachable, damit Alexa weiß, dass das Gerät verfügbar ist
  • actions sind die erlaubten Aktionen, die alle vordefiniert sind

additionalApplianceDetails ist ein privates Feld und wird von Alexa nicht ausgewertet. isReachable könnte z.B. im Winter auf false stehen. Dann reagiert Alexa auf den Einschaltbefehl mit „Dieses Gerät ist derzeit nicht erreichbar“, ohne dass man die Antwort explizit programmieren muss.

Aktion ausführen

Beim Ausführen teilt Alexa lediglich mit, was erkannt wurde. Hier muss dann der API-Endpunkt bei Husqvarna aufgerufen werden.

function handleControl(event, context) {

  console.log('### Control Function received: ' + event.header.namespace);
  /**
   * Fail the invocation if the header is unexpected. This example only demonstrates
   * turn on / turn off, hence we are filtering on anything that is not SwitchOnOffRequest.
   */
  if (!event.header || !event.payload || event.header.namespace != 'Alexa.ConnectedHome.Control') {
    context.fail('TurnOnOffRequest', 'UNSUPPORTED_OPERATION', 'Unrecognized operation');
  } else {

    /**
     * Retrieve the appliance id and accessToken from the incoming message.
     */
    var applianceId = event.payload.appliance.applianceId;
    var manufacturerName = event.payload.appliance.additionalApplianceDetails.manufacturerName;
    var knxAddress = event.payload.appliance.additionalApplianceDetails.ga;
    var accessToken = event.payload.accessToken.trim();
    console.log('### Control Function for device: ' + applianceId);
    console.log('### Control Function target ga: ' + knxAddress);
    console.log('### Control Function Manufacturer recognized ' + manufacturerName);

    var headers = {
      namespace: event.header.namespace,
      payloadVersion: event.header.payloadVersion,
      messageId: event.header.messageId
    };

    var payload = {
      "manufacturer": manufacturerName,
      "v": 0,
      "applianceId": applianceId,
    };

    var result = {
      "header": headers,
      "payload": payload
    };
    // 
    switch (event.header.name) {
      case 'TurnOnRequest':
        console.log('### TurnOnRequest');
        result.payload.v = 1;
        result.header.name = 'TurnOnConfirmation';
        break;
      case 'TurnOffRequest':
        console.log('### TurnOffRequest');
        result.payload.v = 0;
        result.header.name = 'TurnOffConfirmation';
        break;
    }
    return result;
  }
}

Dies ist eine generische Funktion, die die Pakete seziert und das erkannte Kommando ermittelt und in ein privates Format umsetzt. Erkannt werden hier:

  • TurnOnRequest
  • TurnOffRequest

Es gibt viele weitere, sodass sich auch hier ein switch anbietet.

Der Weg zu Husqvarna

Diese Funktion wird mit den aggregierten Daten aufgerufen, wenn Alexa das Kommando erkannt hat und die passende Datenstruktur zur Verfügung stellt. Hier wird dann folgendes übergeben:

  • appliance: applianceId, die selbst vergeben wurde
  • v: Der Wert der Aktion, hier 1 oder 0 für START und PARK
  • callback: Ein Rückruf, der Alexa über Erfolg oder Misserfolg informiert
function sendHusqvarna(appliance, v, callback) {

  function login(username, password, callback) {
    let headers = {};
    headers["Content-type"] = "application/json";
    headers["Accept"] = "application/json";

    let req = https.request({
      host: AUTHURL,
      path: AUTHURL_PATH + 'token',
      method: "POST",
      headers: headers
    }, (response) => {
      response.setEncoding('utf8');
      response.on('data', (body) => {
        console.log('### Husqvarna login response: ' + body);

        let pattern = JSON.parse(body);

        let headers = {
          'Authorization': "Bearer " + pattern.data.id,
          'Authorization-Provider': pattern.data.attributes.provider
        }
        callback(headers);
      });
    });
    req.write(JSON.stringify({
      data: {
        type: "token",
        attributes: {
          username: username,
          password: password,
        }
      }
    }));
    req.end();
  }

  function sendCommand(command, headers, callback) {
    headers["Content-type"] = "application/json";
    headers["Accept"] = "application/json";
    let req = https.request({
      host: TRACKURL,
      path: TRACKURL_PATH + 'mowers/161510367-162200378/control',
      method: "POST",
      headers: headers
    }, (response) => {
      response.setEncoding('utf8');
      response.on('data', (data) => {
        console.log('### Husqvarna command response: ' + data);
        callback();
      });
    });
    req.write(JSON.stringify({
      "action": command
    }));
    req.end();

  }

  login(MOWER_USER, MOWER_PW, (headers) => {
    console.log('Mower Login successfull. Executing command: ' + appliance);
    switch (appliance) {
      case "api_r2d2":
        sendCommand(v === 0 ? 'PARK' : 'START', headers, () => {
          callback();
        })
        break;
    }
  })
}

Der Prozess ist zweistufig. Zuerst muss man sich anmelden. Da kommt ein Token zurück, der folgendermaßen aussieht:

"data": 
   {
        "type": "token",
        "id": "7fd40c2d-0978-44d8-a161-e1b4f6d629a9",
        "attributes": {
             "provider": "husqvarna",
             "user_id": "5ec3abaa-fac6-2605-1964-f764d5505153",
             "expires_in": 864000,
             "refresh_token": "c98013e6-26a2-4b26-ba46-56c1d0506f98"
         }
    }
};

Mit dem kann dann das Kommando abgesetzt werden. Das passiert alles am Ende des Code-Blocks im Aufruf von ‚login‘. Es ist technisch nicht notwendig, sich jedesmal anzumelden, denn der Token hat eine gewisse Lebensdauer („expires_in“: 864000). Aber es ist eigentlich einfacher zu Programmieren, denn so muss man sich die Daten nicht merken, was in den flüchtigen Lambda-Funktionen mehr Aufwand wäre.

Aktivierung

Die Lambda-Funktion muss eventuell mit dem Smarthome-Skill verbunden werden. Dazu dienen Trigger:

Nun wird das Skill bei Alexa aktiviert und das Gerät „discovered“. Wenn die Einrichtung wie zuvor beschrieben auf der Vorlage für Smarthome-Skills basiert, sollte der Trigger schon vorhanden sein.

Und nun?

Jetzt kann man den Rasenmäher mit „Alexa, Robbi einschalten“ losschicken und mit „Alexa, Robbi ausschalten“ wieder einfangen. Seine Hütte findet er dann alleine. Am Besten einen Echo-Dot auf die Terrasse und dann muss man sich überhaupt nicht mehr bewegen.

Fehlersuche

Die vielen Dienste haben reichlich Platz für Fehler. Wenn es nicht geht hilft nur systematische Suche. Zuerst ist wichtig, dass alle Dialoge und Funktionen fehlerfrei beendet wurden. Wenn irgendeine Warnung auftaucht, ist diese ernst zu nehmen. Es macht keinen Sinn, hier weiterzumachen.

Alexa hat praktisch keine Debugging-Funktionen. Bei Lambda sieht es besser aus. Wichtig sind viele Ausgaben mit console.log und dann der Abfruf mittels Cloudwatch. Fast alles lässt sich so lösen. Wenn Lambda überhaupt nicht reagiert, ist es in der Regel die Authentifizierung, die fehlschlägt. Hier nochmal sorgfältig alles anschauen: Amazon Login, Alexa Skill, IAM Role, Lambda Function (in dieser Reihenfolge).

Husqvarna Automower Smarthome-Skill für Alexa