やってみること
「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の設定
基本的には**公式セットアップ方法**に従って設定する。ただし、最後までセットアップはしないこと!下記のような画面でセットアップを途中でやめる
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リクエスト先の設定
{"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を指定するだけで良いので、今回はボタンを押したログも保存するようにします
※logId、createdAt、macAddressをjson形式で持つイメージ
4.AWS API Gatewayの準備
詳細な説明は省きますが、今回はボタンを押されたときにPOSTを受ける口を用意して、受けた先をLambdaにしておきます。
ポイントとしては、下記の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へのリクエストの実施 (サンプルコードは下記) |
'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);
}
やってみて感じたこと
- 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を使えば、「サーバレスアーキテクチャー」をより手軽に実現できるのが素晴らしいと感じました。