myThingsとAmazon Dash ButtonをAWS経由で繋げてみる

# やってみること
「[myThingsで勤怠連絡ボタンを作ってみた](https://techblog.yahoo.co.jp/advent-calendar-2016/mythings-attendance-contact-button/)」を参考に、Amazon Dash Buttonを押したらAWS経由のmyThings Developersで連絡を送れる的なことをやってみたいと思います！

## 用意するもの
* Amazon Dash Button
* AWS API Gateway
* AWS DynamoDB
* AWS Lambda
* myThings Developers

## myThings Developersの準備
こちらは「[myThingsで勤怠連絡ボタンを作ってみた](https://techblog.yahoo.co.jp/advent-calendar-2016/mythings-attendance-contact-button/)」をご参照ください
Slackの設定などは丸々同じものを利用するで問題ございません。

## 手順
1.Amazon Dash Buttonの設定
![AmazonDashButton](http://blogs.c.yimg.jp/res/blog-b2-2e/hcksx908/folder/943905/24/37371424/img_32_m)
基本的には**[公式セットアップ方法](https://www.amazon.co.jp/gp/help/customer/display.html/ref=amb_link_1?ie=UTF8&nodeId=201746340&pf_rd_m=AN1VRQENFRJN5&pf_rd_s=merchandised-search-leftnav&pf_rd_r=TSXWXYC79BGFWDKJCQD3&pf_rd_r=TSXWXYC79BGFWDKJCQD3&pf_rd_t=101&pf_rd_p=cf13d39a-e3f0-4ff4-a61f-037a02d68c1d&pf_rd_p=cf13d39a-e3f0-4ff4-a61f-037a02d68c1d&pf_rd_i=4752863051)**に従って設定する。ただし、最後までセットアップはしないこと！下記のような画面でセットアップを途中でやめる

<img src="http://blogs.c.yimg.jp/res/blog-b2-2e/hcksx908/folder/943905/24/37371424/img_33_m" alt="セットアップ画面" title="セットアップ画面" width="200" height="400" />

2.dasherの設定
[dasher](https://github.com/maddox/dasher)という、libpcapを利用してボタンのMACアドレスからのパケット送信を捉え、設定ファイルに書かれたURLにリクエスト投げるものがあるので、今回はそちらを利用します。
2-a. dasherの準備 (環境によっては、npmとlibpcap-devのインストールが必要です)

```
$ git clone https://github.com/maddox/dasher.git
$ cd dasher
$ npm install
```

2-b. Amazon Dash ButtonのMACアドレスの読み取り。下記コマンドを実行し、Amazon Dash Buttonを押す。

```
$ ./script/find_button 
Watching for arp & udp requests on your local network, please try to press your dash now
Dash buttons should appear as manufactured by 'Amazon Technologies Inc.' 
Possible dash hardware address detected: XX:XX:XX:XX:XX:XX Manufacturer: unknown Protocol: arp
Possible dash hardware address detected: XX:XX:XX:XX:XX:XX Manufacturer: unknown Protocol: udp

※期待値は「Amazon Technologies Inc.」のようですが、実際は「unknown」のものが出てくると思います
```

2-c. HTTPリクエスト先の設定

```config/config.json
{"buttons":[
  {
    "name": "Morning rest button",
    "address": "XX:XX:XX:XX:XX:XX",
    "timeout": "5000",
    "url": "<<のちに用意するAWS API GatewayのURL>>",
    "method": "POST",
    "headers": {"x-api-key": "<<AWS API Gatewayで取得するAPIキー>>"},
    "json": true,
    "body": {"macAddress": "XX:XX:XX:XX:XX:XX"}
  }
]}
```

2-d. dasherの起動。ボタンを押して下記のようなものがコマンドに流れれば完了

```
$ sudo npm run start

> dasher@1.1.1 start /Users/yumatsud/git/dasher
> node app.js

[2016-12-10T15:33:38.687Z] Morning rest button added.
[2016-12-10T15:33:49.339Z] Morning rest button pressed.
```

3.AWS DynamoDBの準備
単純にmyThings Developersにリクエストを投げるだけであれば、上記のdasherのリクエストURLに先に設定したwebscript.ioのURLを指定するだけで良いので、今回はボタンを押したログも保存するようにします
![API Gateway](http://blogs.c.yimg.jp/res/blog-b2-2e/hcksx908/folder/943905/24/37371424/img_35_m)
※logId、createdAt、macAddressをjson形式で持つイメージ

4.AWS API Gatewayの準備
詳細な説明は省きますが、今回はボタンを押されたときにPOSTを受ける口を用意して、受けた先をLambdaにしておきます。
![API Gateway](http://blogs.c.yimg.jp/res/blog-b2-2e/hcksx908/folder/943905/24/37371424/img_34_m)
ポイントとしては、下記の3つ

* 誰でもAPIにリクエストできるのも良くないので、最低限APIキーの設定
* レスポンスにはパラメータがおかしかったときなどように400エラーも設定
* GETでボタンを押したログの一覧を取得でき、さらにmacAddressを指定するとMacAddressに紐づくログも取れる部分も作成

5.AWS Lambdaの準備
AWS API GatewayとLambda関数の対応表は下記

| リソース | メドッド | Lambdaファンクション | 説明 |
|:-----------------|:------------------|:------------------|:------------------|
| /v1/logs | GET | list_logs | 全てのログ一覧取得 |
| /v1/logs | POST | created_log | ログ登録 |
| /v1/logs/{macAddress} | GET | get_logs | MACアドレスごとのログ一覧取得 |
その中でも、created_log関数部分で、DynamoDBへの保存とmyThings Developersへのリクエストの実施 (サンプルコードは下記)

```created_log.js
'use strict';

//var uuid = require('node-uuid');
const qs = require("querystring");
var https = require("https");
var date = new Date();
var dateString = createPostTimeString();

var AWS = require("aws-sdk");
var dynamo = new AWS.DynamoDB.DocumentClient();

// myThings Developersに必要なリクエスト項目
var appid = "<<appid>>";
var secret = "<<secret>>";
var accessToken = "<<access token>>";
var refreshToken = "<<refresh token>>";

exports.handler = (event, context, callback) => {
    //console.log("handler start. event:", JSON.stringify(event, null, 2));
    // パラメータチェック
    if (!event.macAddress) {
        context.fail('macAddress is not specified')
    }
    
    // uuidを生成
    //var uuid = uuid.v4();
    var uuid = createUuid();
    
    // 更新内容をセット
    var item = {
        "logId": uuid,
        "createdAt":  Math.floor(date.getTime() / 1000),
        "macAddress": event.macAddress
    };
    
    var param = {
        TableName: 'Button',
        Item: item
    }
    dynamo.put(param, function(err, data) {
        if (err) {
            context.fail(err);
        } else {
            //context.succeed(item)
            // myThings Developersへリクエスト
            requestDevelopers(context);
        }
    });
};

/**
 * UUID(ランダム文字列)の生成
 * @return string UUID
 */
function createUuid() {
    var S4 = function() {
        return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
    }   
    return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4() +S4());
}

/**
 * "2016/12/21(水)"の形式の文字列を返す
 * @return string 日付文字列
 */
function createPostTimeString() {
    // 年
    var year = date.getFullYear();
    // 月
    var month = date.getMonth() + 1;
    if (month < 10) {
        month = '0' + month;
    }
    // 日
    var day = date.getDate();
    if (day < 10) {
        day = '0' + day;
    }
    // 曜日
    var weekDayList = [ "日", "月", "火", "水", "木", "金", "土" ];
    var weekDay = weekDayList[ date.getDay() ];
    
    return year+"/"+month+"/"+day+"("+weekDay+")";
}

/**
 * myThings Developersへのリクエスト
 * @return void
 */
function requestDevelopers(context) {
    // リクエストパラメータの生成
    var postArgs = {
        date: dateString
    }
    var postData = qs.stringify({
        "entry": JSON.stringify(postArgs),
    });
    
    // リクエスト設定
    var options = {
        hostname: "mythings-developers.yahooapis.jp",
        path: "/v2/services/<<要設定>>/mythings/<<要設定>>/run",
        port: 443,
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
            "Authorization": "Bearer " + accessToken,
        },
    };
    
    // リクエスト
    var req = https.request(options, function(res){
        // 401のとき
        if (res.statusCode == 401) {
            // コールバック付きのrefreshAccessTokenを呼ぶ
            refreshAccessToken(context);
        }

        // レスポンス処理
        res.on("data", function(body){
            var parseData = JSON.parse(body);
            if(typeof( parseData["flag"] ) != "undefined") {
                context.succeed("カスタムトリガーの実行リクエストを受け付けました。")
            } else {
                console.log("カスタムトリガーの実行リクエストの受付に失敗しました。:"+body);
                //context.fail("カスタムトリガーの実行リクエストの受付に失敗しました。:"+body);
            }
        });
    })
    .on("error", function(res){
        context.fail("カスタムトリガーの実行リクエストの受付に失敗しました。:"+res.content);
    });
    req.end(postData)
}

/**
 * アクセストークンのリフレッシュ
 */
function refreshAccessToken(context) {
    console.log("refreshAccessTokenにきたよ");
    // リフレッシュ用データのセット
    var reqData = qs.stringify({
        "grant_type": "refresh_token",
        "refresh_token": refreshToken
    });
    // リクエスト設定
    var buffer = new Buffer(appid + ":" + secret, "ascii");
    var options = {
        hostname: "auth.login.yahoo.co.jp",
        path: "/yconnect/v1/token",
        port: 443,
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
            "Authorization": "Basic " + buffer.toString("base64"),
        }
    };

    // リクエスト実行
    var req = https.request(options, function(res) {
        console.log("refreshAccessTokenのrequestのなかにきたよ");
        // 401の場合
        if(res.statusCode == 401) {
            context.fail("リフレッシュトークンの有効期限が切れました。myThings Developersのサンプルコードからリフレッシュトークンを再取得して下さい。");
        } else if(res.statusCode != 200) {
            context.fail("カスタムトリガーの実行リクエストの受付に失敗しました。:"+res.content);
        }

        // レスポンス処理
        res.on('data', function(body){
            var parseData = JSON.parse(body);
            accessToken = parseData['access_token'];
            requestDevelopers(context);
        }); 
    });

    // POSTデータのリクエスト
    req.end(reqData);
}
```

6.実行結果
![IMG_4081.GIF](https://qiita-image-store.s3.amazonaws.com/0/71637/efa7fe92-afdb-abc6-a90d-951b691b5fa9.gif)
うまくいきました！！

# やってみて感じたこと
* Amazon Dash Buttonとdasherを動かすマシンが同一ネットワーク内にないといけない
* dasherを動かすマシンが別途必要（PCでも可能）

課題はあるものの、マシンもラズパイとかを用意しておいておけば良いのと、手軽にハード(ボタン)を準備できるのは良いですね。
何より電源が不要で、枕元に置いておけば手軽に押せるので、実際に使っても良いと思えるものでした！！

# ちなみに
今回はAPI Gateway -> Lambda -> DynamoDBという構成でしたが、DynamoDBのストリームという機能を使えば、

* API Gateway -> DynamoDB -> Lambda -> myThings Developers

とか、DynamoDBへのinsert時にカスタマイズしたい場合は、

* API Gateway -> Lambda -> DynamoDB -> Lambda -> myThings Developers

というような構成にしても良いかもしれません。
AWSを使えば、「サーバレスアーキテクチャー」をより手軽に実現できるのが素晴らしいと感じました。

# 参考
* [myThingsで勤怠連絡ボタンを作ってみた](https://techblog.yahoo.co.jp/advent-calendar-2016/mythings-attendance-contact-button/)
* [Amazon Dash ButtonをただのIoTボタンとして使う](http://qiita.com/jsoizo/items/3b8bba4160f41aef20f4)