RACCOON TECH BLOG

株式会社ラクーンホールディングス 技術戦略部より、
tipsやノウハウなど技術的な話題を
発信いたします。

Alexaのスマートホームスキルの作り方!IFTTTをトリガーと言わずに操作できるようにする

開発チームの下田です。

こないだ第3世代のecho dotが発売されました。が、第3世代のecho dotは独自仕様の電源で、ちょっと不便だと思いました。
そこでなくならないうちにUSB給電の5V 2Aで動作する第二世代のecho dotを購入しました。
足りない機能は自分で開発できて楽しいです。Alexaに自然に語りかけるようにIFTTTを操作できるスキルを作ってみたので、手順を説明します。

IFTTTとは

IF This Then Thatの頭文字から名前をつけられたサービスで、直訳すると「これならば何かする」という意味です。
例えば、「3時になった」ならば「おやつの催促をメール」するといったことができるサービスです。
時間だけでなく、特定のURLにリクエストがあったらというwebhookといったこともトリガーになります。

検索するとIFTTTわかりやすい解説記事がたくさんあるので、今回は詳しい説明を省略します。

Alexaで操作できること

echoシリーズではAIであるAlexaを利用可能で、対応した機器であれば「エアコンの電源をつけて」とか「テレビのチャンネルをNHKにかえて」などと指示できます。
対応した機器では「エアコンの電源をつけて」のように自然なワードで起動できます。「エアコンつけて」、「リビングのエアコンの電源オフ」などと言い換えても動作します。

対応した機器でなくとも、IRKitなどのIoTデバイスなら割と簡単にAlexaから操作できます。IoTデバイスはIFTTTのアプレットから指示を出せるものが多いです。IFTTTはAlexaのスキルが登録されているので、「トリガー エアコンの電源つけて」などと指示できます。

IFTTTスキルの場合、IFTTTを呼び出すためのワード「トリガー」を必ずつけて言わなければなりません。また、設定した単語にしか反応しません。例えば、「エアコンの電源つけて」と設定しても「エアコンオン」では動作しません。両方動作させるには両方の設定が必要です。

いったい何が違うのか?

Alexaのスキルには種類があります。
前者はデバイス操作用のスマートホームスキルで、Alexaのインターフェースに沿って実装すると、ワードはいい感じにAlexaが解釈してくれます。
後者は汎用のカスタムスキルで、ワードの解釈はスキルに任されています。また、「トリガー」のような特定のワードが必要になるので、ちょっと使い勝手が悪いです。

やっぱりいい感じに操作したい!

IFTTTのカスタムスキルを使った方法でもやりたいことはできるにはできる・・のですが、せっかくスマートスピーカーを使うからにはAIがいい感じにやってほしいと思いました。

ということは、「スマートホームスキルからIFTTTのwebhookにリクエストすれば、いい感じにたいていのことができる」ことになるのではと考えたので、実装してみました。

Alexa スマートホームスキルの概要

スマートホームスキルは3段階のステップがあります。

アカウントリンク

スマートホームスキルはIoTデバイスの操作用なので、アカウント設定が必要です。アカウント設定がないと他の人にデバイスを操作されかねません
なので、スマートホームスキルではデバイスのアカウントと連携する「アカウントリンク」が必須となっています。アカウントリンクではOAuth2による連携が必須となっています。

※ここのところを開発するとこの記事では足りないので省略し、直接IDをコードに記入して自分専用のアプリを作る前提とします。

デバイスの登録

スマートホームスキルは1つでも、デバイスが複数になる場合があります。例えばリビングの照明と寝室の照明の2つを操作するときです。
スマートホームスキルをAlexaに登録したあとにデバイスを見つけ出し(discover)、登録する必要があります。

デバイスの操作

スマートホームスキルとデバイスの登録が完了したら、ようやくそのデバイスを操作できます。

スマートホームスキルの作成

それではスマートホームスキルを作成していきます。
スマートホームスキルはAlexa Developer Consoleから登録できます。
ここでは必ず自分が使用するechoを登録しているAmazonアカウントを使用してください。同じアカウントでないと、今回開発したスキルを使用できません。
おそらくこの記事の読者は日本のamazon、amazon.co.jpのアカウントを利用されているかと思いますので、そちらを利用してください。
初回ログイン時には開発者アカウントの情報登録が挟まります。

ログインが完了するとスキルの一覧画面が表示されるので、「スキルの作成」ボタンをクリックしてください。
スキル名、言語、種類を選択します。ここでは「ifttt_webhook」、日本語、スマートホームを選択しました。

アカウントリンクを実装

OAuth2によるアカウントリンクを設定していきます。とりあえず動けばいいので、詳細は省略し手順を記載します。
左のタブにアカウントリンクがあるので、選択してください。
下の方にリダイレクト先のURLがあるのでメモしておきます。

OAuth2の認可サーバを用意します。ここではAmazonアカウントによる認証を行えるLogin with Amazonを利用します。アカウントはどれでも大丈夫です。
ログインしたら左ペインにある「新しいアプリケーションを登録する」を選択します。

名前、詳細、プライバシー規約URLを入力します。今回は公開しないので全部適当でOKです。

作成したら、WEB設定を開き編集、さきほどメモしたリダイレクト先のURLを入力、保存します。

その後、クライアントID、Client Secretをメモします。

Alexa Developer Consoleに戻り、先ほど開いたアカウントリンクに入力していきます。

認証画面のURI: https://www.amazon.com/ap/oa
アクセストークンのURI: https://api.amazon.com/auth/o2/token
クライアント ID: メモしたクライアントID
クライアントシークレット: メモしたClient Secret
クライアントの認可方法: HTTP Basic認証
スコープ: profile
ドメインリスト: なし
デフォルトのアクセストークンの有効期限: なし

入力したら保存します。

アカウントリンクをテストする

正常にアカウントリンクが設定できたかテストしてみましょう。
Amazon Alexaを開きます。左ペインの「スキル」を選択、右ペインの右上「有効なスキル」を選択すると作成したifttt_webhookが表示されています。
ifttt_webhookを選択し、「有効にする」をクリックするとAmazonによる認証を求めらますので、認証します。
「正常にリンクされました」と表示されればOKです。

デバイスの登録を実装

次はデバイスの登録を実装します。

AWS Lambda関数の作成・連携

Alexa Developer ConsoleのスキルIDをメモしておきます。

ここから先はAWSのLambdaで実装してきます。
関数の作成をクリックします。

一から作成
名前: ifttt_webhook
ランタイム: Node.js 8.10
ロール: 1つ以上のテンプレートから新しいロールを作成します。
ロール名: 適当なロール名
ポリシーテンプレート: 基本的な Lambda@Edge のアクセス権限 (CloudFront トリガーの場合)

次にAlexa Smart Homeトリガーを追加します。トリガーの追加からAlexa Smart Homeを選択すると、下の方にスキルIDを入力する欄が表示されます。そこに先程メモしたスキルIDを入力し追加します。追加したら、右上にある保存ボタンから保存して完了です。
保存ボタンの上にあるARNをメモしておきます。

Alexa Developer Consoleに戻り、メモしたARNをデフォルトのエンドポイントに入力し保存します。これで作成したスマートホームスキルとLambda関数が連携しました。

コーディング

ようやくコードを書きます。スタートするまでが長くてつらいですね。

Lambdaに戻り、作成した関数名をクリックすると、下部にコード入力欄が表示されます。
ハンドラに「index.handler」を設定しておくとスマートホームスキルのイベントが発生したタイミングでindex.jsのexport.handlerに登録したfunctionが呼び出されます。

下記のコードを入力し、保存します。

index.js

exports.handler = (request, context) => {
  console.log(request)
}

保存したら、すぐ左のテストボタンをクリックします。

イベント名にDiscovery、テキストエリアに下記コードを入力します。

{
    "directive": {
        "header": {
            "namespace": "Alexa.Discovery",
            "name": "Discover",
            "payloadVersion": "3",
            "messageId": "1bd5d003-31b9-476f-ad03-71d471922820"
        },
        "payload": {
            "scope": {
                "type": "BearerToken",
                "token": "access-token-from-skill"
            }
        }
    }
}

※messageIdはリクエストごとにユニークなIDが採番されますが、テストでは固定の適当な値です

保存すると、テストの左のドロップダウンリストに作成したテストが追加されます。選択した状態でテストボタンをクリックします。

クリックするとコードの下に実行結果が表示されます。

このあたりのテストの方法は公式ドキュメントにもわかりやすく解説されているので、参照してみると良いでしょう。

Discoveryの要件

デバイスの追加ができるよう、実装していきます。
- 第1引数にリクエスト、第2引数にコールバックが渡される
- directive.headerに{ "namespace": "Alexa.Discovery", "name": "Discover"}が渡されたら、どんなデバイスがあるかコールバックする

次のコードになります。

exports.handler = (request, context) => {
    switch(request.directive.header.namespace) {
      case 'Alexa.Discovery' : 
          handleDiscovery(request, context)
          break
  }
}

// Discovery用のハンドラ
function handleDiscovery(request, context) {
    switch(request.directive.header.name) {
        case 'Discover' :
            let header = request.directive.header;
            header.name = "Discover.Response";
            let payload = {
                    "endpoints": [
                        {
                            "endpointId": 'ifttt_test_1',         // デバイスの固有ID。ここでは適当につける
                            "manufacturerName": "テスト株式会社", // デバイスのメーカー
                            "friendlyName": "エアコン",           // Alexaに呼びかけるときの名前
                            "description": "エアコンのスイッチ",  // 説明
                            "displayCategories": ["THERMOSTAT"],  // デバイスの種類。エアコンだったらTHERMOSTAT、テレビならTVなど
                            "capabilities": [
                                {
                                  "type": "AlexaInterface",
                                  "interface": "Alexa",
                                  "version": "3"
                                },
                                { // 電源の制御ができるものは、Alexa.PowerControllerインターフェースをつける
                                    "interface": "Alexa.PowerController",
                                    "version": "3",
                                    "type": "AlexaInterface",
                                    "properties": {
                                         "retrievable": true
                                    }
                                }
                            ]
                        }
                    ]
                }
            context.succeed({
                event: {
                    header: header, 
                    payload: payload
                }
            })
            break
    }
}

作成したら、Discoveryのテストしてみます。テストボタンを押したら、レスポンスにsucceedに渡したデータが入っていればOKです。

テストできたら、Alexaに戻り端末の検出を行ってみます。
「エアコン」というデバイスが追加されていればOKです。

操作を実装する

まずIFTTTでwebhooksをトリガーにするアプレットを登録します。
IFTTTの詳しい手順は省略します。

そしてLambdaでさらに実装します。
最終的に下記のようになりました。


const ENDPOINT = "ifttt_test_1"
exports.handler = (request, context) => {
    switch(request.directive.header.namespace) {
      case 'Alexa.Discovery' : 
          handleDiscovery(request, context)
          break
      case 'Alexa.PowerController' : 
          handlePowerControl(request, context)
          break
  }
}

// iftttにリクエストする
function requestIfttt(key) {
    let https = require("https")
    return new Promise((resolve, reject) => {
        // keyは各自で違う
        https.get(`https://maker.ifttt.com/trigger/${key}/with/key/XXXXXX-99999999999999`, res => resolve())
            .on('error', error => reject(error))
    })
}

// 電源管理用のハンドラ
function handlePowerControl(request, context) {
    let powerResult = ""
    // 一応Alexaは電源の状態を持つので、結果を返す
    let requestResult
    switch(request.directive.header.name) {
        case 'TurnOn' :
            requestResult = requestIfttt('aircon_hot')
            powerResult = "ON"
            break
        case 'TurnOff' :
            requestResult = requestIfttt('aircon_off')
            powerResult = "OFF"
            break
    }
    requestResult.then(() => {
        console.log('then')
        var responseHeader = request.directive.header
        responseHeader.namespace = "Alexa"
        responseHeader.name = "Response"
        responseHeader.messageId = responseHeader.messageId + "-R"
    
        var response = {
            context: {
                "properties": [{
                    "namespace": "Alexa.PowerController",
                    "name": "powerState",
                    "value": powerResult,
                    "timeOfSample": (new Date()).toISOString(),
                    "uncertaintyInMilliseconds": 50
                }]
            },
            event: {
                header: responseHeader,
                endpoint: {
                    scope: {
                        type: "BearerToken",
                        token: request.directive.endpoint.scope.token
                    },
                    endpointId: ENDPOINT
                },
                payload: {}
            }
        }
        context.succeed(response)
        
    })
}

// Discovery用のハンドラ
function handleDiscovery(request, context) {
    switch(request.directive.header.name) {
        case 'Discover' :
            let header = request.directive.header;
            header.name = "Discover.Response";
            let payload = {
                    "endpoints": [
                        {
                            "endpointId": ENDPOINT,         // デバイスの固有ID。ここでは適当につける
                            "manufacturerName": "テスト株式会社", // デバイスのメーカー
                            "friendlyName": "エアコン",           // Alexaに呼びかけるときの名前
                            "description": "エアコンのスイッチ",  // 説明
                            "displayCategories": ["THERMOSTAT"],  // デバイスの種類。エアコンだったらTHERMOSTAT、テレビならTVなど
                            "capabilities": [
                                {
                                  "type": "AlexaInterface",
                                  "interface": "Alexa",
                                  "version": "3"
                                },
                                { // 電源の制御ができるものは、Alexa.PowerControllerインターフェースをつける
                                    "interface": "Alexa.PowerController",
                                    "version": "3",
                                    "type": "AlexaInterface",
                                    "properties": {
                                         "retrievable": true
                                    }
                                }
                            ]
                        }
                    ]
                }
            context.succeed({
                event: {
                    header: header, 
                    payload: payload
                }
            })
            break
        case 'Alexa.PowerController' : 
            handlePowerControl(request, context)
            break
    }
}

実装できたらテストしてみます。
電源管理用のリクエストするテストを追加したいので、テストボタンの左のドロップダウンリストからテストイベントの設定を選択します。

新しいテストイベントの作成を選択し、適当な名前をつけ、テキストエリアに下記のコードを入力します。

{
  "directive": {
    "header": {
      "namespace": "Alexa.PowerController",
      "name": "TurnOn",
      "payloadVersion": "3",
      "messageId": "1bd5d003-31b9-476f-ad03-71d471922820",
      "correlationToken": "dFMb0z+PgpgdDmluhJ1LddFvSqZ/jCc8ptlAKulUj90jSqg=="
    },
    "endpoint": {
      "scope": {
        "type": "BearerToken",
        "token": "access-token-from-skill"
      },
      "endpointId": "appliance-001",
      "cookie": {}
    },
    "payload": {}
  }
}

思ったとおりの動きになれば完成です。

完成したらAlexaに話しかけてみてください。
「エアコンの電源をオン」
「エアコンをきって」
「エアコンつけて」
など、自然な言葉で操作できるようになったはずです。

まとめ

以上がスマートホームスキルの実装方法です。
今回はどなたでも応用が効くようにwebhookによる実装にしましたが、ここまでできてしまえばお持ちのデバイスに合わせた方法に変えるのは簡単だと思います。
また電源のオンオフ以外にもチャンネルの変更やボリューム上げ下げなど、デバイスに合わせたインターフェースが用意されているので、実装してみても面白いと思います。

おまけ:東京のチャンネル番号を取得する

Alexa.ChannelControllerインターフェースでチャンネルが取得できますが、「テレビのチャンネルをNHKに変えて」と指示してもrequest.directive.payload.channel.numberに値が入ってきません。
東京の主要chは対応を調べたので、のせておきます。

function getChannelNumber(request) {
    switch(request.directive.payload.channelMetadata.name) {
        case 'nhk' :
            return '1'
        case 'e テレ' :
            return '2'
        case '日テレ' :
            return '4'
        case 'テレビ朝日' :
            return '5'
        case 'テレ東' :
            return '7'
        case 'tbs' :
            return '6'
        case 'フジ' :
            return '8'
        case 'tokyo mx' :
            return '9'
        default :
            return request.directive.payload.channel.number
    }
}

関連記事

運営会社:株式会社ラクーンホールディングス(c)2000 RACCOON HOLDINGS, Inc