AWS IoTを使って水を発注してみる

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、せーのです。今日はAWS IoTを使った弊社の業務効率化施策をひとつご紹介します。

水なんて誰が発注してもいい

クラメソの札幌オフィスにはウォーターサーバが設置されています。

aws_iot_water1

みんなここからお水を飲んだり、コーヒーを淹れたりしています。

ウォーターサーバは中のお水が切れたら新しいのに入れ替えますね。で、お水のストックがなくなりそうになったら業者さんに発注をかけて在庫を補充してもらいます。

aws_iot_water2

発注はお金がかかることなのである程度の権限をもった人に限られています。ですがそれは逆に言うとその人が病欠していたり、出張に行っているとわざわざ連絡して発注だけしてもらう、という非常に属人的で不毛な作業が発生することを指します。
権限をもった人がやりたいことは「発注業務の管理(適切なタイミングで適切な量を発注しているかどうか)」であって発注作業そのものではありません。またクラメソでは「属人的な業務 = 悪」という方程式が存在します。更に言うと発注作業はメールで業者に「◯箱発注お願いします」と頼むのですが、このメールの作成がめんどくさい。

ということでこの課題をIoTを使って解決します。

解決法

まずプッシュボタンを用意し、Raspberry Piに繋げます。ボタンが押されたらラズパイが作動しMQTTでAWS IoTにデータを投げます。データ、と言っても"water"という文字列を固定で投げるだけです。AWS IoTはそれを受けたらSNSを叩きます。SNSからは固定されたメールアドレスにメールを投げます。これでボタンを押せば水が届くようになります。

aws_iot_water3

次に細かい仕様を決めます。

管理者にもメールを

SNSからメールを送るには送り先のアドレスからの承認が必要となります。業者さんのメアドを直接指定すると業者さんに電話して「今AWSから送られているメールの承認リンクを押してもらえませんか」というようなやり取りが発生し先方にお手間を取らせてしまいます。また業者さんにメールを送るのと同じタイミングで管理者にもコピーメールを送り「発注したよ」ということを知らせる必要があります。
SNSは複数の宛先に同時にメールを送れるのでそこに管理者も登録してしまえばいいのですが、今回は業者さんの手間を作らない、という要件もありましたので社内にメーリングリストを作り、そこに業者さん含め管理者のメアドも登録することにしました。手順としては

  • メーリングリストを作成し、自分のメアドを入れる
  • SNSの宛先にメーリングリストを登録し、自分に届いたメールから承認を押す
  • メーリングリストに業者さんと管理者のメアドを登録
  • 自分のメアドを外す

と、こんな感じです。

aws_iot_water4

重複防止

誰でもボタンを押せば発注出来るようになったため、当然違う人が少し前に発注したことを知らずにもう一度ボタンを押してしまうことが考えられます。またボタンを押しただけでは発注できたかどうかわからないのでもう一度押してしまう人が出てくることも懸念されます。

そこでまず発注されたことがわかるように発注が完了したら社内のChatworkに通知を流すことにしました。つぎに重複防止としてSQSを使うことにしました。発注したらSQSにキューを登録し、そのキューが存在している間は新たな発注をブロックする、という仕様です。キューの長さは7日間としました。AWS IoTのRuleには複数のActionが設定できるのですが今回は「SQSへの登録、チェック」「SNSへのリクエスト送信」「Chatworkへの通知」が同期的に動く必要があるのでLambdaをかまし、そこで同期的に処理を書くことにします。

aws_iot_water6

さあ、仕様は固まりました。実装しましょう。

やってみた

AWS IoTの構築

まずはAWS IoTの構築から初めましょう。証明書を新規に作ります。

aws_iot_water7

今はワンクリックで証明書、秘密鍵、公開鍵がダウンロードできます。便利ですね。

aws_iot_water8

次にRuleを作ります。Lambdaをここで設定する必要があるので適当に空のLambdaを作っておきます。

aws_iot_water9

なんとこれだけでAWS IoTの設定はOKです。

Raspberry Piの実装

次にラズパイの実装です。今回はnode.jsで組みたいと思います。証明書を任意のフォルダにセットしたらコードを書きます。ボタンは21番ポートに繋げました。

var mqtt = require('mqtt');
var fs = require('fs');

fs.writeFileSync('/sys/class/gpio/export', 21);
fs.writeFileSync('/sys/class/gpio/gpio21/direction', 'in');

var awsIot = require('aws-iot-device-sdk');
var device = awsIot.device({
	  	host: 'XXXXXXXXXXXXXX.iot.ap-northeast-1.amazonaws.com',
	  	port: 8883,
   keyPath: '/root/cert/private.pem.key',
  certPath: '/root/cert/certificate.pem.crt',
    caPath: '/root/cert/rootCA.pem',
  clientId: 'water_order',
    region: 'ap-northeast-1'
});


device
  .on('message', function(topic, payload) {
    console.log('message', topic, payload.toString());
  });

var value = 0;
var data = {"order": "water"};
while( 1 ) {
    value = fs.readFileSync('/sys/class/gpio/gpio21/value', 'ascii');
    console.log( value );

    if ( value == 1 ) {
       sendAws();
    }
}
function sendAws(){

	device.publish('water/order', JSON.stringify(data));
	console.log('send AWS IoT.');

}

device.on('connect', function() {
  console.log('mqtt client connect');
});

多少単純化していますが、要はボタンが押されたらMQTTを通じてAWS IoTにデータを投げているだけです。

SQSの構築

SQS部分の構築です。キューは7日間で消えるように設定してみました。

aws_iot_water10

SNSの構築

次はSNSです。これも単純にリクエストが来たらその内容をメールする、というだけです。

aws_iot_water11_1

Lambda Functionの実装

最後にこれらをまとめるLambda Functionの実装です。

console.log('Loading function');

var request = require('request');
var room_id = '999999999';

var cw_options = {
    url: 'https://api.chatwork.com/v1/rooms/'+room_id+'/messages',
    headers: {
        'X-ChatWorkToken': 'XXXXXXXXXXXXXXXXXXXXXXXXXXXX'
    },

    form: { body: '' },
    json: true
};

var aws = require('aws-sdk');
var sns = new aws.SNS({apiVersion: '2010-03-31'});
var sqs = new aws.SQS({apiVersion: '2012-11-05'});

var snsParams = {
  Message: 'ご担当者様\n\n毎々お世話になっております、クラスメソッド水担当です。\n\n水ボックスについて発注させて頂きたく存じます。\n\n追加:天然水ボックス=5箱\n\n以上よろしくお願いいたします。\n\n====================================================\nクラスメソッド株式会社\n  AWSコンサルティング部 英打部龍生(aws_o@classmethod.jp)\nURL:https://classmethod.jp/\nTEL:111-1111-1111 / 222-2222-2222 (AWSコンサルティング部直通)\n====================================================',
  Subject: '水ボックス発注のお願い',
  TopicArn: 'arn:aws:sns:us-east-1:999999999999:Classmethod_water'
};

var sqsSendParams = {
  MessageBody: 'Water has already ordered.', /* required */
  QueueUrl: 'https://sqs.us-east-1.amazonaws.com/999999999999/water_order', /* required */
  DelaySeconds: 0
};

var sqsreadParams = {
  QueueUrl: 'https://sqs.us-east-1.amazonaws.com/999999999999/water_order'
};

exports.handler = function(event, context) {

    sqs.receiveMessage(sqsreadParams, function(err, data) {
      if (err) {
        console.log(err, err.stack); // an error occurred
      } else {

        if (data.Messages && data.Messages.length > 0){
          console.log('water as already ordered');
          cw_options.form.body = 'お水は少し前に頼んでおいたから今回はキャンセルしためそ!';
          request.post(cw_options, function (error, response, body) {
              if (!error && response.statusCode == 200) {
                 console.log(body);
                 context.succeed("water canceled.");
              }else{
                  console.log('error: '+ response.statusCode);
              }
          });
        } else {
          sns.publish(snsParams, function() {
                  console.log('sns published.');
                  sqs.sendMessage(sqsSendParams, function(err, data) {
                    if (err) {
                      console.log(err, err.stack);
                      context.fail("sqs cannot queue.");
                    } else {
                      console.log('sqs queued.');
                      cw_options.form.body = 'お水頼んどいてあげためそ!';
                      request.post(cw_options, function (error, response, body) {
                          if (!error && response.statusCode == 200) {
                             console.log(body);
                             context.succeed("water ordered.");
                          }else{
                              console.log('error: '+ response.statusCode);
                          }
                      });

                    }
                  });
          });
        }
      }
    });


};

実際に書いてみるとそんなに難しくはないかと思います。

テスト

テストです。ボタンを押してみます。

aws_iot_water12

チャットに通知が届きました。メールを確認してみます。

aws_iot_water14_1

きちんとメールが飛びました。ではもう一回押してみます。

aws_iot_water13

チャットに通知が届きました。メールは、、、飛んでいないようです。そして今日、無事に水は届きました。成功です。

まとめ

いかがでしたでしょうか。IoTを使った簡単な実例でした。今後はラズパイにAVS(Alexa Voice Services)を入れて、音声で注文してみたいと思います。