1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

kintone に保管したセンサ値を AWS Lambda で一定間隔でチェックし警告をメールで送信

Last updated at Posted at 2019-01-08

処理内容

・15分毎に AWS Lambda を起動します。
・kintone アプリから警告用の設定を取得します。
・kintone アプリからセンサ値を取得します。
・センサ値が警告レベルなら AWS SES で警告メールを送信します。
・kintone アプリの警告状況を更新します。

kintone の設定

・計測値を保管するアプリを追加して、計測値を保管します。
・警告メールの送信条件を保管するアプリを追加して、設定を保管します。(同時に警告の発生状況を保管。)
・kintone のアプリには Lambda の API リクエストのための、 APIトークンを設定しておきます。

AWS SES の設定

AWS SES でメールを送信します。
今回は、送信者認証でメールを送信できるように設定し、利用します。
設定は以下を参考にしてください。

Amazon SESによるメール送信環境の構築と実践
https://dev.classmethod.jp/cloud/aws/amazon-ses-build-and-practice/

自動でメール配信をしたい!Amazon SESで効率的なメール配信をしよう!
https://style.potepan.com/articles/11667.html

AWS Lambda の設定

AWS Lambda の設定については以下を参考にしてください。

今度こそ理解する!俺式Lambda入門
https://dev.classmethod.jp/cloud/aws/lambda-my-first-step/

AWS Lambda 関数を cron のように定期実行
https://www.qoosky.io/techs/89d52682fb

Lambda のコード

以下は sync-request を利用していますが、node.js が 7.6 以降であれば then-request と Promise await で実装することをお勧めします。

Promise, async, await がやっていること
https://qiita.com/kerupani129/items/2619316d6ba0ccd7be6a

then-request
https://www.npmjs.com/package/then-request

Node.jsのHTTP通信はsync-request非推奨でthen-requestを推奨
https://designetwork.daichi703n.com/entry/2017/02/21/node-then-request

index.js
'use strict';

var aws     = require('aws-sdk');
var ses     = new aws.SES({apiVersion: '2010-12-01', region: 'ap-northeast-1' });

var request = require('sync-request');

var domain     = "cybozu.com";
var subdomain  = "SUBDOMAIN";
var path       = "/k/v1/records.json";
var protocol   = "https://";

var setAppId   = "KINTONE_SETTING_APP_ID";
var setToken   = "SETTING_KINTONE_APP_TOKEN";

var dataAppId  = "KINTONE_DATA_APP_ID";
var dataToken  = "DATA_KINTONE_APP_TOKEN";

var to       = ['TO_MAIL_ADDRESS'];  // AWS SES でverify済みのメールアドレス(送信先)
var from     = 'FROM_MAIL_ADDRESS';  // AWS SES でverify済みのメールアドレス(送信元)

exports.handler = function(event, context) {
    // 15 分前を算出
    require('date-utils');
    var dt = new Date();
    var dtAgo = new Date();
    dtAgo.setTime(dt.getTime() - (15 * 60 * 1000));
    
    // kintone アプリからデータチェック用設定情報を取得
    var url            = protocol+ subdomain + '.' + domain + path;
    var query          = "Status" + 'in ("有効") order by PlaceId asc limit ' + count;
    var settingRecodes = funcGetKintoneRecodes(request, setAppId, url, setToken, query);
    
    // kintone アプリから現在のデータを取得
    var query       = "DateTime" + ' < "' + dtAgo.toFormat("YYYY-MM-DDTHH24:MI:SSZ") + '"' + ' order by PlaceId asc, DateTime desc limit 50';
    var dataRecodes = funcGetKintoneRecodes(request, dataAppId, url, dataToken, query);
    
    // 取得したデータのチェック
    var errorData = funcCheckData(settingRecodes, dataRecodes);
    if(errorData.length > 0){
        funcSendMails(ses, to, from, errorData);
        funcPutRecodes(request, url, setAppid, setToken, errorData);
    }
};

// データチェック処理
function funcCheckData(settingRecodes, dataRecodes)
{
    var recodes = [];
    if (settingRecodes.length == 0 || dataRecodes.length == 0){
        return recodes;
    }
    
    // 計測場所毎のデータに集約
    var dataPlaceId = "";
    var uniqueData = [];
    dataRecodes.forEach(function(record){
        if(dataPlaceId != dataRecodes['PlaceId']['value']){
            uniqueData[dataRecodes['PlaceId']['value']]['DateTime']          = dataRecodes['DateTime']['value'];
            uniqueData[dataRecodes['PlaceId']['value']]['Temperature']       = dataRecodes['Temperature']['value'];
            uniqueData[dataRecodes['PlaceId']['value']]['Humidity']          = dataRecodes['Humidity']['value'];
            uniqueData[dataRecodes['PlaceId']['value']]['CO2']               = dataRecodes['CO2']['value'];
            uniqueData[dataRecodes['PlaceId']['value']]['SaturationDeficit'] = dataRecodes['SaturationDeficit']['value'];
            dataPlaceId = dataRecodes['PlaceId']['value'];
        }
    });
    
    var i = 0;
    settingRecodes.forEach(function(record){
        // 場所毎に集約したデータを設定値と突き合わせ
        var settingPlaceId = record['PlaceId']['value'];
        if(uniqueData[settingPlaceId] != null){
            var isNormal = true;
            var item = "";
            switch (record['DataType']['value']){
                case "温度":
                    item = "Temperature";
                    break;
                case "湿度":
                    item = "Humidity";
                    break;
                case "飽差":
                    item = "SaturationDeficit";
                    break;
                case "CO2濃度":
                    item = "CO2";
                    break;
            }
            
            // 警告対象外(通常値)かどうか判断
            if(record['Condition']['value'] == "以上" 
              && record['Value']['value'] <= uniqueData[settingPlaceId][item]){
                isNormal = false;
            }
            if(record['Condition']['value'] == "以下" 
              && record['Value']['value'] >= uniqueData[settingPlaceId][item]){
                isNormal = false;
            }
        }
        
        // 警告を通知
        if(!isNormal && (record['StartDateTime']['value'] == "" || (record['StartDateTime']['value'] != "" &&  record['EndDateTime']['value'] != ""))){
            recodes[i]['Normal']   = isNormal;
            recodes[i]['Subject']  = "<警告発生>" + record['Title']['value'];
            recodes[i]['Body']     = "場所:" + record['Title']['PlaceName'] + "\n";
            recodes[i]['Body']    += "設定:" + record['DataType']['value'] + "" + record['Value']['value'] + " " + record['Condition']['value'] + " 現在値( " + uniqueData[settingPlaceId][item] + "";
            recodes[i]['Body']    += "\n" + record['Message']['value'];
            recodes[i]['No']       = record['Title']['レコード番号'];
            recodes[i]['DateTime'] = uniqueData[settingPlaceId]['DateTime'];
            i++;
        // 警告を解除
        }else if(isNormal && record['StartDateTime']['value'] != "" && record['EndDateTime']['value'] != ""){
            recodes[i]['Normal']   = isNormal;
            recodes[i]['Subject']  = "<警告解除>" + record['Title']['value'];
            recodes[i]['Body']     = "場所:" + record['Title']['PlaceName'] + "\n";
            recodes[i]['Body']    += "設定:" + record['DataType']['value'] + "" + record['Value']['value'] + " " + record['Condition']['value'] + " 現在値( " + uniqueData[settingPlaceId][item] + "";
            recodes[i]['No']       = record['Title']['レコード番号'];
            recodes[i]['DateTime'] = uniqueData[settingPlaceId]['DateTime'];
            i++;
        }
    });
    return recodes;
}

// 警告メールを送信
function funcSendMails(ses, to, from, errorData)
{
    errorData.forEach(function(record){
        funcSendMail(ses, to, from, record['Subject'], record['Body']);
    });
}

// 警告状況をkintoneの設定ファイルに反映
function funcPutRecodes(request, url, appid, token, errorData)
{
    errorData.forEach(function(record){
        var id = record['No'];
        var json;
        if(record['Normal']){
            json = { "EndDateTime": record['DateTime'] };
        }else{
            json = { "StartDateTime": record['DateTime'] };
        }
        funcPutKintoneRecode(request, url, appid, token, id, json)
    });
}

// メールの送信
function funcSendMail(ses, to, from, subject, body)
{
    var eParams = {
        Destination: {
            ToAddresses: to
        },
        Message: {
            Body: {
                Text: {
                    Data: body,
                    Charset: 'iso-2022-jp'
                }
            },
            Subject: {
                Data: subject,
                Charset: 'iso-2022-jp'
            }
        },
        Source: from
    };
    
    var email = ses.sendEmail(eParams, function(err, data){
        if(err){
            context.fail(new Error('Mail error occured.'));
        } else {
            context.succeed('Mail sucsess.');
        }
    });
}

// kintone のデータを取得する
function funcGetKintoneRecodes(request, appId, url, token, query)
{
    if(count > 500) count = 500;
    var appUrl = url + '?app=' + appId + '&query=' + encodeURI(query);
    var respons = request('GET', appUrl, {
        'headers': {
            'X-Cybozu-API-Token': token
        }
    });
    return JSON.parse(respons.getBody());
}

// kintone のデータを更新する
function funcPutKintoneRecode(request, url, appid, token, id, json)
{
    var options = {
        url: url,
        method: 'PUT',
        headers: {
            'Content-type': 'application/json',
            'X-Cybozu-API-Token': token
        },
        body: { app : appid, id : id, record: json },
        json: true
    };
	
    request.put(options, function (err, res, body) {
        console.log('response' + res.statusCode);
        if (!err && res.statusCode === 200) {
            console.log('response SUCCESS!!');
        } else {
            console.log('response error: ' + res.statusCode);
        }
        console.log('response end');
    });
}

参照情報

AWS Lambda ( https://aws.amazon.com/jp/lambda/ )
AWS SES ( https://aws.amazon.com/jp/ses/ )
【Python3】AWS 「API Gateway」「Lambda」「SES」を使って、RaspberryPiでのセンサー検知をメール通知してみた ( http://raspi.seesaa.net/article/431653483.html )
AWS SES+Lambda で作る、ドメインまるっとメール転送 ( http://iseebi.hatenablog.com/entry/2016/05/05/123815 )
sync-request ( http://designetwork.hatenablog.com/entry/2016/11/19/sync-request-post-node-js )
then-request ( https://www.npmjs.com/package/then-request )

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?