トイレをIoT化した話 ~Internet of Toilet~(後編)

#はじめに
RHEMS技研の西田です。
ところで皆さん(特に男性)、立ち小便派ですか？
弊社オフィスでは立ち小便用便器がないので禁止です。なのに何故掃除する時に便器の周りに尿がハネているのでしょうか。。
そんな事で、今回は立ち小便した人をslackで ~~公開処刑~~ 通知する仕組みを作ってみました。

#使用技術
Serverless Framework
AWS
| - Lambda
| - API Gateway
| - DynamoDB
ESP-WROOM-02(開発ボード)

#概要図
![now.jpeg](https://qiita-image-store.s3.amazonaws.com/0/249725/21bca626-2ea8-f69b-4159-f82f7bf89407.jpeg)


#流れ
ESP-WROOM-02を使って、トイレの便座が開閉時や機器の電源投入時に、APIGatewayで発行したエンドポイントURIにPOSTメソッドでボディを付けデータを送信。 - ①/②
送られてきたボディをLambdaで解析し、現在の状況の取得と、最新の状況をDynamoDBに書き込む。 - ③
開いたよ！メッセージが届いた時のみslackで作ったアプリのWebhookUrlにリクエスト送信 - ④

本記事ではServerless Frameworkを使ってAWSを設定した所まで(③ ~ ④)を扱います。ESP-WROOM-02(① ~ ②)については[こちら](https://qiita.com/Akijin/items/3e1bde9f260fa14d7d4b)の記事で紹介しています。

#詳細
今回、一連の処理をするAWSのサービス群はServerless Frameworkを使って管理しています。

```yml:serverless.yml
service: sls-rhems-iot

frameworkVersion: ">=1.1.0 <2.0.0"

provider:
  name: aws
  runtime: nodejs6.10
  region: ap-northeast-1
  environment:
    DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"

functions:
  manage:
    handler: lambdas/manage.manage
    events:
      - http:
          path: rhems/manage/{id}
          method: put
          cors: true
      - http:
          path: rhems/manage/{id}
          method: get
          cors: true
      - http:
          path: rhems/manage/{id}
          method: delete
          cors: true

  wciot:
    handler: lambdas/wciot.wciot
    events:
      - http:
          path: rhems/wc
          method: post
          cors: true

resources:
  Resources:
    DynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          -
            AttributeName: id
            AttributeType: S
        KeySchema:
          -
            AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
```

lambdaではnode.js6.10を使いました。
また、関数はマネージメント用(今後トイレ以外にも追加する予定があるため)、トイレ用の2つを用意しています。

```javascript:manage.js
'use strict';

const AWS = require('aws-sdk');
const db = new AWS.DynamoDB.DocumentClient();

module.exports.manage = (event, context, callback) => {
    let params = {
        TableName: process.env.DYNAMODB_TABLE,
        ConsistentRead: true,
        Key: { id: event.pathParameters.id, },
    };
    let response = { };

    switch (event.httpMethod) {
        case 'DELETE': //------------------------------------------------------------------
            db.delete(params, (err) => {
                if (err) {
                    response.statusCode = err.statusCode || 501;
                    response.body = 'Couldn\'t remove item.';
                } else {
                    response.statusCode = 200;
                    response.body = JSON.stringify(params.Key);
                }
                callback(null,response);
            });        
            break;
        case 'GET': //------------------------------------------------------------------
            db.get(params, (err, result) => {
                if (err) {
                    response.statusCode = err.statusCode || 501;
                    response.body = 'Couldn\'t fetch item.';
                } else {
                    response.statusCode = 200;
                    response.body = JSON.stringify(result.Item);
                }
                callback(null,response);
            });
            break;
        case 'PUT': //------------------------------------------------------------------
            params.Item = {
                id: event.pathParameters.id,
                message: "new"
            };
            db.put(params, (err) => {
                if (err) {
                    response.statusCode = err.statusCode || 501;
                    response.body = 'Couldn\'t create item.';
                } else {
                    response.statusCode = 200;
                    response.body = JSON.stringify(params.Item);
                }
                callback(null,response);
            });
            break;
        default:
            response.statusCode = 500;
            response.body = 'Unexpected error';
            callback(null,response);
    }
};
```

IoT装置で扱うエンドポイントは全てPOSTメソッドで、ボディを {id:NUM,　message:STRING} の形で受け取ります。

```javascript:wciot.js
'use strict';

const request = require('request');
const AWS = require('aws-sdk');
const db = new AWS.DynamoDB.DocumentClient();
const timestamp = new Date().getTime();

module.exports.wciot = (event, context, callback) => {
    
    const data = JSON.parse(event.body);
    let params = {
        TableName: process.env.DYNAMODB_TABLE,
        ConsistentRead: true,
        Key: {
            id: data.id,
        },
        ExpressionAttributeValues: {
            ':message':data.message,
            ':updateAt': timestamp
        },
        UpdateExpression: 'set message = :message, updateAt=:updateAt',
        ReturnValues: 'UPDATED_NEW'
    };

    const notificationPromise = function (options) {
        return new Promise(function (resolve, reject) {
            request(options, function (error, response, body) {
                if (!error && response.statusCode == 200) {
                    resolve(body);
                } else {
                    reject(error);
                }
            });
        });
    };
    db.update(params).promise()
        .then(function (result) {
            return new Promise(resolve => {
                let promiseObject = {};
                if (data.message == "ON") {
                    promiseObject.notificationFlag = true;
                    promiseObject.notificationMessage = "電源がONになりました";
                } else if ((data.message == "UP")) {
                    promiseObject.notificationFlag = true;
                    promiseObject.notificationMessage = "便座が上がってます!!!";
                }
                promiseObject.response = {
                    statusCode: 200,
                    body: JSON.stringify(result.Attributes),
                }
                resolve(promiseObject);
            });
        }).then(object => {
            if (object.notificationFlag) {
                const slackOption = {
                    url: 'https://hooks.slack.com/services/hOge1/fUga2/Piyopiyo3',
                    method: 'POST',
                    headers: {
                        'Content-type': 'application/json'
                    },
                    body: JSON.stringify({
                        "text": object.notificationMessage
                    })
                };

                notificationPromise(slackOption).then(slackResult => {
                    console.log(slackResult);
                }).catch(function (err) {
                    callback(err);
                });
            }
            callback(null, object.response);

        }).catch(function (err) {
            callback(err);
        });

};
```

また、今回はrequestライブラリを読み込んでいるので、`` npm install request ``をしています。

#やってみた結果
<img width="227" alt="スクリーンショット 2018-07-24 19.58.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/249725/9cc88809-6284-99d5-008b-bf12bca06489.png">
うまく行きました!
掃除する時は電源を切ることで対応をしています。


#今後
　Rekognitionを使って、**トイレのドアの前に**カメラをつけて、誰が立ち小便をしたかを検知できるようにする予定です。
![future.jpeg](https://qiita-image-store.s3.amazonaws.com/0/249725/5248dcdf-8f6d-dec4-bc05-35d73fb6a2b9.jpeg)