myThingsで勤怠連絡ボタンを作ってみた

  • このエントリーをはてなブックマークに追加

Yahoo! JAPAN Tech Advent Calendar 2016の15日目の記事です。一覧はこちら

こんにちわ。
ヤフー株式会社スマートデバイス推進本部 IoT推進部の松田です。

初めてこのようなブログに投稿させて頂きます。
読みづらい文章などあるかもしれませんが、暖かい目で読んでいただけると嬉しいです。

突然ですが

みなさん、日常生活の中で、こんなときありませんか。

  • 朝起きたら、体調が悪くて、午前中は様子見のためにもお休みしたい
  • 前日飲みすぎた、午前中は動くのもしんどいので午後から会社にいこう

……「それが許される会社なんてあるのか!?」とお思いかもしれませんが、そこは置いておいて、実際にこういうことってありますよね。

弊社ですとメールで勤怠報告することが多いのですが、
こんなとき、パソコンやスマホを開いてメールを送るのは、
テンプレートを作っていたりしても面倒ですよね。
体調が悪いからこそすぐにもう一度眠りにつきたいのに、この作業で目がすっかり覚めてしまったり(コラ)

そんなとき私はこう思いました(実体験)
「ボタンとか押したら、勤怠連絡とかいい感じにで送ってくれたらなー」

そしてこう思いました
「そっか、作ればいいのか。」

というわけで、前置きが長くなりましたが、ちゃちゃっと作ってみようと思います!

前提

下記のツールを使う。

  • myThings
  • AWS (いろいろと組み合わせて使ってみる)
  • 勤怠連絡はYahoo!メールとSlack

そして今回はあまりハードウエアをいじらずにソフトウエアをいじるだけ、かつサーバを用意しないで何とかできないか模索してみました。

連絡フォーマット

  • メール
件名:【勤怠】午前半休:<<日時>>:<<氏名>>
本文:
○○さん

お疲れさまです。<<氏名>>です。
本日体調不良により、午前半休を取得いたします。
午後も様子を見て出社しようと思います。
ご迷惑おかけいたしますが、よろしくお願いいたします。
  • Slack
○○さん

お疲れさまです。<<氏名>>です。
本日体調不良により、午前半休を取得いたします。
午後も様子を見て出社しようと思います。
ご迷惑おかけいたしますが、よろしくお願いいたします。

やってみたこと

いろいろと試しながらやってみたので下記にまとめて書いていこうと思います。

①myThingsアプリのみでやってみた

myThings
この myThingsアプリ にあるボタンチャンネルでやってみます

手順

  1. myThingsアプリをインストール
  2. トリガーをボタンチャンネルに設定して、アクションにYahoo!メールSlackを設定
    組み合わせ作成画面

  3. 作成内容 -> 【午前半休連絡!
    シェア画像

  4. 実行
    実行画面
  5. 結果
    ■メール
    メール実行結果
    ■Slack
    Slack実行結果

やってみての課題

  • スマホを開くのが面倒
  • アプリを起動するのが面倒
  • 時間などを動的にいれたい場合のカスタマイズができない

これはこれで既に便利なのですが上記の課題があるので、別な方法を検討してみることに。

②Hackeyデバイスを使ってみた

Hackeyというデバイス、ボタン型にもできると聞いてやってみた
スイッチ型Hackey
引用:Cerevo、Webと繋がる“鍵”「Hackey」発売、9980円

用意するもの

準備

1.Hackeyをマニュアルに沿ってボタン型にする
Hackey

2.myThings Developersの設定 (詳細は開発者ドキュメントの開発フローをご確認ください)
■組み合わせの設定
トリガーにカスタムトリガー、アクションにYahoo!メールとSlackを設定
組み合わせ設定

■トリガーの設定
カスタムトリガーを選択し、設定項目の「トリガー名」「リクエスト項目」を下記のように設定
トリガー設定

■アクションの設定
左がYahoo!メールの設定で、右がSlackの設定
アクション設定

3.webscript.ioでユーザー設定画面遷移部分を作成(下記はサンプル。疎通確認のサンプルコードから<<>>部分を適宜入力してください)

-- Token EndPoint --
local auth_url = 'https://auth.login.yahoo.co.jp/yconnect/v1/token'

-- ユーザー設定画面URL取得APIエンドポイント --
local setting_url = '<<setting url>>'

-- Application ID & Secret --
local appid = '<<appid>>'
local secret = '<<secret>>'

-- Access Token & Refresh Token --
local access_token = '<<access token>>'
local refresh_token = '<<refresh token>>'

-- ユーザー設定画面URL取得関数 --
function get_setting_url(setting_url, access_token)
  local res = http.request {
    url = setting_url,
    headers = {
      ['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8',
      ['Authorization'] = 'Bearer '..access_token
    }
  }
  return res
end

-- ユーザー設定画面URL取得 --
local response = get_setting_url(setting_url, access_token)

if response.statuscode == 401 then
  local ret = http.request {
    url = auth_url,
    headers = {
      ['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8',
      ['Authorization'] = 'Basic '..base64.encode(appid..':'..secret)
    },
    data = {
      grant_type = 'refresh_token',
      refresh_token = refresh_token
    },
    method = 'POST'
  }
  if ret.statuscode == 401 then
    return 'リフレッシュトークンの有効期限が切れました。myThings Developersのサンプルコードからリフレッシュトークンを再取得して下さい。'
  elseif ret.statuscode ~=200 then
    return 'ユーザー設定画面URLの取得に失敗しました:'..ret.content
  end
  local dec = json.parse(ret.content)
  access_token = dec['access_token']
  log(access_token)
  response = get_setting_url(setting_url, access_token)
end

if response.statuscode ~=200 then
  return 'ユーザー設定画面URLの取得に失敗しました:'..response.content
end

local result = json.parse(response.content)

if result['url'] ~= '' then
  return 301, '', {
    location = result['url']
  }
end

return 'ユーザー設定画面URLの取得に失敗しました:'..response.content

4.webscript.ioで組み合わせ実行部分を作成(下記はサンプル。疎通確認のサンプルコードから<<>>部分を適宜入力してください)

-- Token EndPoint --
local auth_url = 'https://auth.login.yahoo.co.jp/yconnect/v1/token'
-- カスタムトリガー実行APIエンドポイント --
local run_trigger_url = '<<trigger url>>'
-- Application ID & Secret --
local appid = '<<appid>>'
local secret = '<<secret>>'
-- Access Token & Refresh Token --
local access_token = '<<access token>>'
local refresh_token = '<<refresh token>>'
-- カスタムトリガー実行関数 --
function run_custom_trigger(run_trigger_url, access_token, params)
    local res = http.request {
      url = run_trigger_url,
      headers = {
          ['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8',
          ['Authorization'] = 'Bearer '..access_token
      },
      method = 'POST',
          data = params
    }
    return res
end
-- custom trigger parameter --
-- 曜日の取得(Luaにswitch分はないので下記) --
weekday_str = '日'
local weekday = os.date("%w")
log(weekday)
if weekday == "1" then
    weekday_str = '月'
elseif weekday == "2" then
    weekday_str = '火'
elseif weekday == "3" then
    weekday_str = '水'
elseif weekday == "4" then
    weekday_str = '木'
elseif weekday == "5" then
    weekday_str = '金'
elseif weekday == "6" then
    weekday_str = '土'
end
local post_args = {
    date = os.date("%Y/%m/%d").."("..weekday_str..")"
}
local post_params = {
    entry = json.stringify(post_args)
}
-- ユーザー設定画面URL取得 --
local response = run_custom_trigger(run_trigger_url, access_token, post_params)
if response.statuscode == 401 then
    local ret = http.request {
      url = auth_url,
      headers = {
          ['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8',
          ['Authorization'] = 'Basic '..base64.encode(appid..':'..secret)
      },
      data = {
            grant_type = 'refresh_token',
            refresh_token = refresh_token
      },
      method = 'POST'
  }
    if ret.statuscode == 401 then
        return 'リフレッシュトークンの有効期限が切れました。myThings Developersのサンプルコードからリフレッシュトークンを再取得して下さい。'
    elseif ret.statuscode ~=200 then
        return 'カスタムトリガーの実行に失敗しました:'..ret.content
    end
    local dec = json.parse(ret.content)
    access_token = dec['access_token']
    response = run_custom_trigger(run_trigger_url, access_token, post_params)
end
if response.statuscode ~=200 then
    return 'カスタムトリガーの実行に失敗しました:'..response.content
end
local result = json.parse(response.content)
if result['flag'] == true then
    return 'カスタムトリガーの実行リクエストを受け付けました。'
end
return 'カスタムトリガーの実行リクエストの受付に失敗しました。:'..response.content

5.HackeyのWebhookに上記4で作成したURLを設定 (詳細はHackeyのWebhook Actionを設定するをご確認ください)
HackeyのWebhook設定

実行結果

  1. Hackeyのボタンを押す (もしくはボタンにできないときは鍵を右に回す)
  2. 結果は下記。Slackは上記のアプリと同じで、メールは日付を動的に入れれました!!
    メールの結果

やってみての課題

  • webscript.io、URLが外にバレると誰でもリクエストできるのでこのままのやり方は危険 (それと無料版は7日間のみ)
  • Hackeyをボタン型に取り付け直す必要があり (鍵のままでもいいんですがw)

これはこれで良かったのですが、肝心のAWSを使えていないので、次はAWSを使う方法でやってみることにします。

③Amazon Dash Buttonをハックしてみる

2016年12月5日から発売になりました「Amazon Dash Button」をゴニョゴニョしてやってみたいと思います。

用意するもの

  • Amazon Dash Button
  • AWS API Gateway
  • AWS DynamoDB
  • AWS Lambda
  • myThings Developers (ここは②で用意したものをそのまま使います)

手順

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リクエスト先の設定

{"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へのリクエストの実施 (サンプルコードは下記)

'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でも可能)

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

他にもやってみたいこと

ここまで色々とやってみましたが、時間がなくてやれなかったことは下記になります。

時間があるときにより便利に、そしてよりイケてるものにしていこうと思います(年末年始の宿題です><)

終わりに

勤怠連絡ボタンと評して色々とやってみました。
世の中には便利なソリューションがたくさんあり、それらを組み合わせていけば欲しいものが簡単に作れちゃいますね。

皆さんも何か身近な課題があれば、IoTで、かつmyThingsを使って解決してみてはいかがでしょうか?

最後に、拙い記事を読んでいただき誠にありがとうござました。

参考

追伸:ホントはAWS IoT Buttonを使いたかった

最後はAmazon Dash Button部分で色々とやってみたましたが、AWS IoT Buttonを使えばもっと簡単にできそうした。
ただ、まだ日本では発売されていないのが残念で仕方がないです。AWS IoT Buttonが待ち遠しいですね。
発売されたらやってみたいと思います!!

おまけ

このAdvent Calendarとは別に、myThings Advent Calendar 2016 も開催していますので、よろしければこちらもどうぞ!

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加