AmazonEcho
AlexaSkillsKit

Alexaに電気とエアコンを操作してもらった(日本語で)

はじめに

本稿はalexa smart home skills と raspberry piを使った赤外線リモコン操作のお話です
すでにSmart Home Skillsを使ってみる#qiitaという素晴らしい記事がありましてほぼ2番煎じとなります
差分は下記4点となります
- 英語(turn on light) → 日本語(電気をつけて)
- echosim.io → echo 実機動作
- smart home skills ペイロード v2 → v3
- リモコン操作(実デバイス)実装

できたもの

動作

alexa_smarthome_ir.gif
ちっちゃい&音無しでごめんなさい
字幕通りに会話してます(信じて)

構成

alexa_demo_smarthome.png

echo dot

alexa smart home skills

lambda

--- リモコン操作デバイス実装 ---

heroku // MQTTブローカー

Raspberry Pi + 赤外LED

電気/エアコン

製作過程

amazon developer 日本アカウント作る

第1の躓きポイント : developer アカウントを作る罠ボタン
失敗しないAlexa開発者アカウントの作り方#classmethodを参考に
普段買い物に使うアカウントでデベロッパーコンソールにログインするだけ
決してアカウント作成してはいけない

何に躓いたかと言うと echo が日本上陸する前に echosim.io で遊ぶため amazon.com のデベロッパーアカウント作ってました
そのままの流れで日本向けスキルを開発していたのですが
alexa smart home skills から lambda が呼び出されず途方にくれていました
alexa smart home skills からの lambda の呼び出しには"言語"と"リージョン"の制約があることを知り
試行錯誤の末、amazon.co.jp のデベロッパーアカウントで skill 作るとうまくいきました
あと天気とかも日本の現在地となってくれました

AWS アカウントを作る

lambdaの実装が必要になるので Amazon Web Service に登録します
アカウントの作り方は参考記事がいっぱいあると思うので割愛します

heroku アカウントを作る

MQTTブローカーを作るために heroku に登録します
アカウントの作り方h(ry
爆速でMQTTのパブリックBroker環境を作る方法を見て heroku 使うことを決めました
# cloudmqtt単体でも使えるみたいですね

alexa smart home skills を作る

ここでやっと本題の smart home skills です
前述のSmart Home Skillsを使ってみる#qiitaを参考にしてもらうとできるとできると思います(丸投げ)
ちょっと違うのは日本アカウントで作っているのでページが日本語になってます
# あっ、最近skill開発のUIが変わってるみたいですね
# switch to old console で古い方にしてもらうと前述の記事と同じ見た目になるはずです

skill が出来たら alexaアプリのスキル一覧に自分の作成したskillがでると思います
アカウントリンクまで出来たら skill 部分は完成です
(デバイス検索時にlambdaで返す情報でskillの振る舞いが決まるため、skillの実装に必要なのはOAuthくらいです)
ただし、lambda を正しく実装しないと"デバイスが見つかりません"となります

トラブルシューティング

Q. アカウントリンクがうまくいかない(リンクが変)
A. 認証 URLを確認してください
?redirect_url=が正しいかを確認してください
クライアントID、アクセストークンURL、クライアントシークレットを確認してください
(私はfirefox使っててクライアントシークレットのコピペになぜかゴミが入っていてうまくいきませんでした)
Q. デバイスが見つからない上に lambda が呼ばれた様子がない
A. 前述の"言語"と"リージョン"の制約と思われます
US-west(オレゴン)で lambda を作ってください
あと、初めの頃、alexaアプリでのデバイス検索が上手くいかなくて、音声で「デバイスを探して」を試したらlambdaが呼ばれた覚えがあります。(曖昧な情報でごめんなさい)

lambda を実装する

第2の躓きポイント : ペイロードバージョン
海外サービスあるあるの、突然のI/F変更
v2→v3で結構変わっています
どっかのサイトからv3で動くサンプル拾ってきました

基本となるのは、デバイス検索要求[Alexa.Discovery]に対する応答と
その応答で登録したデバイスの種類ごとのリクエスト[今回はAlexa.PowerController]に対する応答です

index.js
exports.handler = function(request, context) {
  if (request.directive.header.namespace === 'Alexa.Discovery'
      && request.directive.header.name === 'Discover') {
    // "デバイスを検索"が実行されると呼ばれる
    log("DEGUG:", "Discover request", JSON.stringify(request));
    handleDiscovery(request, context, "");
  }
  else if (request.directive.header.namespace === 'Alexa.PowerController') {
    // handleDiscovery() でPowerControllerという種類のデバイスで登録するので
    // そのイベントハンドラ @see: https://developer.amazon.com/ja/docs/device-apis/alexa-powercontroller.html
    if (request.directive.header.name === 'TurnOn'
        || request.directive.header.name === 'TurnOff') {
      log("DEBUG:", "TurnOn or TurnOff Request", JSON.stringify(request));
      handlePowerControl(request, context);
    }
  }
}

コード全文はgithubにあげています
こちらに

Alexa.Discoveryに対する応答

{ event: { header: {}, payload: {} } }
という形で、header には要求でもらったjsonのheaderをそのまま流用し
nameDiscover.Responseに変えたら応答の形になります

payloadにはこのskillが管理するデバイス(複数可)をendpointsに配列で入れて返します

endpoint.json
{
  "endpointId": "light(lambdaで処理する際に使う名前)",
  "manufacturerName": "Smart Device Company(会社名?通信するときにxxxに接続中とかで使われている)",
  "friendlyName": "電気(ユーザーが呼び出しに使う名前)",
  "description": "smart switch for light(alexaアプリ上で見える説明文)",
  "displayCategories": ["SWITCH(alexaアプリ上でのデバイスアイコンの種類)"],
  "cookie": {}, // 用途が分かってないので使ってません
  "capabilities": [
    {
      "interface": "Alexa(必須のようです)",
      "type": "AlexaInterface",
      "version": "3"
    },
    {
      "interface": "Alexa.PowerController(スイッチon/offのIF)",
      "version": "3",
      "type": "AlexaInterface",
      "properties": {
        "supported": [{ "name": "powerState" }],
        "retrievable": true
      }
    }
  ]
}

詳しくは公式ドキュメント

Alexa.PowerControllerに対する応答

MQTTでデバイスにメッセージ送る処理とAlexa skill kit に応答を返す処理の2つを実装します
MQTTでメッセージ送る際に接続待ちが発生するためpromiseを使って送信完了まで待ってから応答を返します

handlePowerControl.js
function handlePowerControl(request, context) {
  // get device ID passed in during discovery
  var requestMethod = request.directive.header.name;
  // get user token pass in request
  var requestToken = request.directive.endpoint.scope.token;
  // get endpointId
  var endpointId = request.directive.endpoint.endpointId;
  var powerResult;

  if (requestMethod === "TurnOn") {
    // Make the call to your device cloud for control 
    powerResult = "ON";
    }
  else if (requestMethod === "TurnOff") {
    // Make the call to your device cloud for control and check for success 
    powerResult = "OFF";
  }

  var mqtt = require('mqtt');
  var mqttpromise = new Promise( function(resolve,reject){
    var options = {
    port: xxxxx,
    clientId: 'mqttjs_' + Math.random().toString(16).substr(2, 8),
    username: "username",
    password: "password",
    };
    var client  = mqtt.connect('mqtt://XXX.cloudmqtt.com', options);

    client.on('connect', function() { // When connected
    // publish a message to any mqtt topic
    client.publish(endpointId, powerResult);
    client.end();
    resolve('Done Sending');
    });

  });
  mqttpromise.then(
    function(data) {
      console.log('Function called succesfully:', data);
      var response = {
        "context": {
          "properties": [{
            "namespace": "Alexa.PowerController",
            "name": "powerState",
            "value": powerResult,
            "timeOfSample": "2017-02-03T16:20:50.52Z",
            "uncertaintyInMilliseconds": 500
          }]
        },
        "event": {
          "header": {
            "namespace": "Alexa",
            "name": "Response",
            "payloadVersion": "3",
            "messageId": "something",
            "correlationToken": "something"
          },
          "payload": {}
        }
      };

      log("DEBUG", "Alexa.PowerController ", JSON.stringify(response));
      return context.succeed(response);
    },
    function(err) {
      console.log('An error occurred:', err);
    }
  );    
}

応答のjsonの中身について
詳しくは公式ドキュメント

赤外線リモコン操作デバイスを作る

リモコン実装

Raspberry Pi + 赤外LED を LIRC で動かしています
インストール手順などは割愛させていただきます
というかだいぶ前に作ったものの流用のため覚えていない;
Raspberry Pi Zero で赤外線リモコンを作る#qiitaなどを参考にすれば作れると思います

lambda との接続

デバイスをクラウドから操作するために今回はMQTTを使いました
理由としては RasPi を port 開けて運用するのが面倒だからです
MQTTを使うとブローカーを介して双方向通信が可能で NAT 越えの手段としてはとてもお手軽です

MQTTの受け取りはPythonで実装しています

sub.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

import paho.mqtt.client as mqtt
import subprocess

host = 'xxx.cloudmqtt.com'
port = 00000

keepalive = 60

def on_connect(client, userdata, flags, rc):
    print('Connected with result code ' + str(rc))
    client.subscribe('light')  # topic名: light を受け取るよう設定
    client.subscribe('aircon') # topic名: aircon を受け取るよう設定


def on_message(client, userdata, msg): # 上で設定した topic が publish されると、ここで受け取る
    print('on_message:' + msg.topic + ',' + str(msg.payload))
    if msg.payload == 'ON':
        cmd = 'on'
    if msg.payload == 'OFF':
        cmd = 'off'

    # 外部コマンドの呼び出しを使って LIRC の送信コマンド irsend を呼んでいます
    # $ irsend SEND_ONCE 機器名 送信コード名
    # この例では topic名が機器名と同じになっています
    retcode = subprocess.call(['irsend', 'SEND_ONCE', msg.topic, cmd])
    print('retcode:'+retcode)


if __name__ == '__main__':
    client = mqtt.Client()
    client.username_pw_set('username','password')
    client.on_connect = on_connect
    client.on_message = on_message

    client.connect(host, port, keepalive)
    client.loop_forever()

あとはpythonをデーモン化するメモ#qiitaを参考にMQTT受け取りをデーモン化して赤外線リモコン操作デバイスは完成です

MQTT通信の疎通確認はcloudmqttのweb上からメッセージ発行できるのでそれで確認できるかと思います

課題

デバイスから状態を返していないためalexaアプリ上(音声でも確認できるのかな?)では実際の状態を反映できません
lambdaの中には状態もてないのでデバイスに聞きに行く処理等の実装が必要となります

おわりに

書き出してみるとごちゃごちゃと長い割に
大事なとこは丸投げみたくなってしまいました
日頃から記録取っておく習慣を持たなければいけませんね
ここまで読んでくださってありがとうございました
質問・指摘大歓迎です