1. Qiita
  2. 投稿
  3. myThingsDevelopers

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

  • 3
    いいね
  • 0
    コメント

やってみること

myThingsで勤怠連絡ボタンを作ってみた」を参考に、Amazon Dash Buttonを押したらAWS経由のmyThings Developersで連絡を送れる的なことをやってみたいと思います!

用意するもの

  • Amazon Dash Button
  • AWS API Gateway
  • AWS DynamoDB
  • AWS Lambda
  • myThings Developers

myThings Developersの準備

こちらは「myThingsで勤怠連絡ボタンを作ってみた」をご参照ください
Slackの設定などは丸々同じものを利用するで問題ございません。

手順

1.Amazon Dash Buttonの設定
AmazonDashButton
基本的には公式セットアップ方法に従って設定する。ただし、最後までセットアップはしないこと!下記のような画面でセットアップを途中でやめる

セットアップ画面

2.dasherの設定
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
※logId、createdAt、macAddressをjson形式で持つイメージ

4.AWS API Gatewayの準備
詳細な説明は省きますが、今回はボタンを押されたときにPOSTを受ける口を用意して、受けた先をLambdaにしておきます。
API Gateway
ポイントとしては、下記の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
うまくいきました!!

やってみて感じたこと

  • 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 Advent Calendar 201622日目の記事です。
Comments Loading...