Alexaスキルで位置情報を取得し、さらに外部APIを利用して住所を取得する

この記事は[スマートスピーカー Advent Calendar 2020](https://qiita.com/advent-calendar/2020/smart-speaker)の6日目の記事です。

#はじめに
個人的に最近自動車のIoT、いわゆるコネクティッドカーサービスに興味があり、実際に便利さを体験したい！と、自分の車のIoT化を進めています。
この計画では、Amazonから車載用エコーデバイスの[EchoAuto](https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/location-services-for-alexa-skills.html)が出たことで、車載Alexaの可能性を感じ、車の中で使うスキル開発も含むことにしました。
実際にスキルの開発をし、既に[燃費計算スキル](https://www.amazon.co.jp/dp/B07NB1RSBP/)をリリースしたのですが、今回その第二弾として位置情報サービスを使ったスキルの開発をはじめました。

そのスキルを作る中で、今までやってきたAlexaスキルでは使うことのなかった機能がありました。それが、タイトルにあるように

1. スキルで向け[位置情報サービス](https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/location-services-for-alexa-skills.html)を使う
2. そこで得た情報を変数として外部APIを実行。情報を取得する

になります。
この記事では、この2つの機能を一連の流れとして動かすことができたので、どうやって実現したか紹介します。
ちなみに、私はハードウェアエンジニアであり、ソフトウェアエンジニアではないため、コードが汚いですし言葉の使い方もおかしいかもしれません。それを踏まえて優しい気持ちでこの記事を読んでいただけると幸いです。

#位置情報サービスについて
位置情報サービスは基本的にスマートフォンアプリの[Alexaアプリ](https://apps.apple.com/jp/app/amazon-alexa/id944011620)を使えるユースケースのみに使えると思ってください。
日本ではEchoAutoやサードパーティで既に販売されているAlexa内蔵のワイヤレスイヤホンなどがこれに該当します。これらはスマートフォンとBluetoothで接続して利用するため、Alexaを利用する時には前述のAlexaアプリを利用する仕組みになっており、位置情報サービスが利用できます。

#開発環境
今回の開発は以下で示す環境で行いました。
 - Node.js 12.x
 - Alexa Hosted-skill (Custom skill)
 - iPhone XS max (ios14.2) →Alexa Appのテストで使用しています。

#位置情報サービスをスキルで使えるようにする
実はスキルで位置情報サービスを使えるようにするのはとても簡単です。
[公式ドキュメント](https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/location-services-for-alexa-skills.html)でもわかりやすく書いてありますが、このページではわかりにくい設定の仕方など不明な点を補足します。

まず位置情報サービスを有効にする時は、スキルを作ったら、[ツール]→[アクセス権限]を選択します。
![スクリーンショット 2020-12-06 22.59.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/e15cef84-0d90-2551-9507-00e3ef509533.png)

アクセス権限を選択すると、位置情報サービスのトグルがあるので、それをONにします。
![スクリーンショット 2020-12-06 23.05.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/2f938fc4-826a-5536-7947-c7e250647970.png)

これで設定は完了です。あとはバックエンドのlambdaで許可が出ていない時はalexaアプリのカードでユーザーにアクセス許可を出す処理を書きます。

#位置情報を取得してみる
実際のhosted skillのコードを載せます。LaunchRequestHandlerのみで行っています。
やっていることは
1. ユーザーがこのスキルで位置情報サービスを使えるようにアクセス許可をしているか確認。
2. 許可してたらhandlerInputから座標の情報を取り出す
になります。

一つ注意があります。
**この位置情報サービスに対応したデバイスでなければ、位置情報は取得できません！ webブラウザでもNGです。
実際に実機(iPhoneのAlexa app)を使わなければ動かないのでご注意ください。**

また細かいコードの説明はコメントアウトに記載しました。

```sample.js

const Alexa = require('ask-sdk-core');

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        var speakOutput = new String();
        var reprompt = new String();

        const isGeoSupported = handlerInput.requestEnvelope.context.System.device.supportedInterfaces.Geolocation; //使っているAlexaデバイスが位置情報サービスに対応しているか確認
        if (isGeoSupported) { //ここでリクエストをしたデバイスがGeolocationサービスに対応しているかを確認（AmazonEchoDotやEchoFlex、EchoAutoなどのデバイスが対応しているかを確認）
            const geoObject = handlerInput.requestEnvelope.context.Geolocation;//対応していたらGeoLocationが取れるはず
            if (!geoObject || !geoObject.coordinate) {//だけどGeolocationが取れない場合。
                //このスキルを使うユーザーが、位置情報サービスの権限を与えていない。
                speakOutput = "'このスキルでは、あなたがお使いのデバイスの位置情報を利用します。デバイスの位置情報をAlexaアプリが利用可能になっていることを確認し、位置情報サービスのアクセス権を設定してください。'"
                handlerInput.responseBuilder
                    .withAskForPermissionsConsentCard(['alexa::devices:all:geolocation:read'])//Alexaアプリでカードを表示し、許可を促す
            } else {//許可もされていて、ちゃんとGeolocationが取得できる。
                //座標情報を取得
                const coordinate = handlerInput.requestEnvelope.context.Geolocation.coordinate//coordinateは座標。Geolocationやドキュメントをみると他にも取れることがわかる。
                console.log(coordinate)
                let latitude = coordinate.latitudeInDegrees //緯度
                let longitude = coordinate.longitudeInDegrees //経度
                console.log("lat:" + latitude + "  lon:" + longitude)

                speakOutput = "座標が取得できたよ"
                handlerInput.responseBuilder
                    .speak(speakOutput)

            }
        } else { //デバイスがGeolocationに対応していない
            speakOutput = "すみません。お使いのデバイスではこのスキルを利用することができません。アレクサアプリまたは、エコーオートでご利用ください"
            handlerInput.responseBuilder
                .speak(speakOutput)
        }

        return handlerInput.responseBuilder
            .getResponse();
    }
};

```

エコーデバイスに話しかけてスキルを起動してみてください。アクセス権限が許可されていれば、ちゃんと座標を取得することができて、cloud watchで現在地の緯度、経度が表示されます。
console.logの結果はこのようになります。

```
{ latitudeInDegrees: 34.80816650390625, longitudeInDegrees: 139.0668590883707, accuracyInMeters: 190.16527590718184 }
```
これで位置情報サービスはOKです。

ただ、こんな感じで座標が取れるとこの場所の住所が知りたくなりますよね。
スキルではこの座標情報を伝えるのではなく、この座標から割り出された住所を喋らせたかったので、住所に変換をする何かが必要でした。
そこで外部APIを使って、この緯度、経度を住所に変換するという方法をとりました。

#緯度経度による住所検索 API
今回は[HeartRails Geo API](http://geoapi.heartrails.com/api.html)という外部APIを使ってみました。
このAPIには緯度、経度の情報から住所を検索するAPIがあり、無償で使うことができます。

#Alexaで外部API(住所検索API)を実行する
今まで外部APIをAlexaスキルで実行したことがなかったので、どうやって非同期処理を書いたらいいか少しわからず苦労しました。
開発途中では、APIのレスポンスを待たずに handlerInput.responseBuilderが返されてしまい、何も喋らない、だけどエラーもしないということがよく起きていました。
ここでやりたい事は、APIのレスポンスを結果を待ってから次の処理が実行されるようにする事です。
以下のコードはそれを解決したコードになります。前述のコードに加筆する形で書いておきます。これをそのままコピペしてもらえば、動きます。

```sample2.js
const Alexa = require('ask-sdk-core');
const http = require('http');//ここでhttpモジュールを追加！！httpモジュールは標準モジュールのため、この行を追加するだけでOK。ローカルでnpm installして・・・などの作業はしなくて良いです。hosted skillでもOKです
//ちなみに、今回使ったAPIが"http"だったのでhttpモジュールを使いましたが、APIが"https"の場合は、httpsモジュールを使ってください

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    async handle(handlerInput) { //ここは非同期にする。理由は外部APIを叩くから。
        var speakOutput = new String();
        var reprompt = new String();

        const isGeoSupported = handlerInput.requestEnvelope.context.System.device.supportedInterfaces.Geolocation; //使っているAlexaデバイスが位置情報サービスに対応しているか確認
        if (isGeoSupported) { //ここでリクエストをしたデバイスがGeolocationサービスに対応しているかを確認（AmazonEchoDotやEchoFlex、EchoAutoなどのデバイスが対応しているかを確認）
            const geoObject = handlerInput.requestEnvelope.context.Geolocation; //対応していたらGeoLocationが取れるはず
            if (!geoObject || !geoObject.coordinate) { //だけどGeolocationが取れない場合。
                //このスキルを使うユーザーが、位置情報サービスの権限を与えていない。
                speakOutput = "'このスキルでは、あなたがお使いのデバイスの位置情報を利用します。デバイスの位置情報をAlexaアプリが利用可能になっていることを確認し、位置情報サービスのアクセス権を設定してください。'"
                handlerInput.responseBuilder
                    .withAskForPermissionsConsentCard(['alexa::devices:all:geolocation:read']) //Alexaアプリでカードを表示し、許可を促す
            } else { //許可もされていて、ちゃんとGeolocationが取得できる。
                //座標情報を取得
                const coordinate = handlerInput.requestEnvelope.context.Geolocation.coordinate //coordinateは座標。Geolocationやドキュメントをみると他にも取れることがわかる。
                console.log(coordinate)
                let latitude = coordinate.latitudeInDegrees //緯度
                let longitude = coordinate.longitudeInDegrees //経度
                console.log("lat:" + latitude + "  lon:" + longitude)

                //外部APIを使って住所情報を取得。発話する文章にまとめてresponseに返す
                try {
                    const response = await CreateMessage(latitude, longitude);//このCreateMessageの処理が終わったら、speakOutput = responseが実行される用意する必要がある。そこで await をつける
                    speakOutput = response;

                    handlerInput.responseBuilder
                        .speak(speakOutput)

                } catch (error) {
                    handlerInput.responseBuilder
                        .speak("すみません、位置情報がうまく取得できませんでした")
                }

                speakOutput = "座標が取得できたよ"
                handlerInput.responseBuilder
                    .speak(speakOutput)

            }
        } else { //デバイスがGeolocationに対応していない
            speakOutput = "すみません。お使いのデバイスではこのスキルを利用することができません。アレクサアプリまたは、エコーオートでご利用ください"
            handlerInput.responseBuilder
                .speak(speakOutput)
        }

        return handlerInput.responseBuilder
            .getResponse();
    }
};


//CreateMessageのfunction
const CreateMessage = function(latitude, longitude) {
    var url = "	http://geoapi.heartrails.com/api/json?method=searchByGeoLocation&x=" + longitude + "&y=" + latitude + ".json" //HeartRails Geo APIのルールにならってURLを入力。最後に.jsonをつけるとjson型としてbodyを取得できる。
    return new Promise((resolve, reject) => { //このPromiseというのが重要！！ 正しい挙動の場合resolveが返され、エラーのときはrejectが返される。
        const request = http.get(url, response => {
            response.setEncoding('utf8');

            let returnData = '';
            if (response.statusCode < 200 || response.statusCode >= 300) {
                return reject(new Error(`${response.statusCode}: ${response.req.getHeader('host')} ${response.req.path}`));
            }

            response.on('data', chunk => {
                returnData += chunk;
            });

            response.on('end', () => {

                let res = JSON.parse(returnData);
                console.log(res.response.location); //取得できた全データを確認
                console.log(res.response.location[0]); //0が現在地から一番近い町地域で情報を示す。

                let location = res.response.location[0];
                let prefecture = location.prefecture;
                let city = location.city;
                let town = location.town;

                console.log(city)
                console.log(town)
                let speakOutput = "あなたは" + prefecture + city + town + "のあたりに居ます"
                resolve(speakOutput);
            });

            response.on('error', error => {
                let errorSpeakOutput = "すみません、取得した位置情報に問題がありました。"
                reject(errorSpeakOutput);
            });
        });
        request.end();
    });
}

```

このコードを実行すると、特にエラーがなければ「あなたは〇〇県XXX市△△のあたりにいます」と話をしてくれます。
ちなみに、```res.response.location[0]```はこんな結果が返ってきます

```
{ city: '賀茂郡東伊豆町', city_kana: 'かもぐんひがしいずちょう', town: '片瀬', town_kana: 'かたせ', x: '139.062165', y: '34.804049', distance: 627.8376744293478, prefecture: '静岡県', postal: '4130303' }
```

##最後に
Alexaスキルで位置情報サービスを初めて使ってみました。位置情報サービスは驚くほど簡単で、Echo Autoとの組み合わせで面白いスキルができるのではないか・・・その可能性を感じました。
しかし、外部APIをAlexaスキルで実行するのはとても難しかった。それでも調べに調べ、USのAlexa開発関連のドキュメントに詳しく書いてあるのを発見！それにより、ちゃんと動くものが作れました。

https://developer.amazon.com/en-US/blogs/alexa/alexa-skills-kit/2019/05/alexa-skill-recipe-update-making-http-requests-to-get-data-from-an-external-api-using-the-ask-software-development-kit-for-node-js-version-2

外部APIは使えると面白いですが、よくわからない場合はこちら参考にしてもらえればと思います。