処理内容
・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
'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 )