人感センサー非感知で電話発信（MESH × kintone × twilio）

# **前置き**
MESH、twilioを連携させるためにデータベースにkintoneを利用してみました。
qiitaへの投稿も不慣れなため、適宜修正していきます。
こんなことも出来るんだなぁ～程度で、読みづらい・分かりづらいところは
ご配慮ください。。。
[- 概要について（SlideShare)](https://www.slideshare.net/ssuser672d562/ss-109925839)

# **目的**
MESHの人感センサーを3台設置し、3台とも非感知状態になったときに
携帯電話へ電話で通知されるシステムを構築する。
（本来の目的は認知症患者等が部屋から無断外出した場合の検知）

# **利用したサービス**
- [MESH](http://meshprj.com/jp/)
- [kintone](https://kintone.cybozu.co.jp/jp/)
- [twilio](https://twilio.kddi-web.com/)
- [AWS EC2（Linux)](https://aws.amazon.com/jp/)

# **概要**
- kintoneへ各種設定情報を登録（人感センサー、通知先）
- MESHから人感センサーの状態をkintoneへ送信/登録（タイマーで定期的に実行）
- MESHからAWSへ配置しておいたPHPをコール（タイマーで定期的に実行）
- PHPにて人感センサーの状態を確認し、3台とも非感知であればtwilioで電話発信

## **kintoneへ各種設定情報を登録（人感センサー、通知先）**
サイボウズのkintoneで設定情報、センサー感知情報を登録するための**アプリ**を作成。
kintoneの使い方などは下記サイボウズの公式サイトを参考に。
- [はじめてのkintoneガイドブック](https://kintone.cybozu.co.jp/jp/2014/images/support/index/welcometokintone.pdf)
- [機能別！便利に使おうガイドブック](https://kintone.cybozu.com/jp/feature/date.html#guidebook)

### **設置センサー一覧**

| フィールド名 | 種別 | 概要 |
|:-----------------|:------------------|:-------------------|
| 部屋番号 | 文字列（1行） | センサーを設定してある部屋番号（場所） |
| センサーシリアル番号 | 文字列（1行） | 設置してあるセンサーのシリアル番号 |

### **通知先設定**

| フィールド名 | 種別 | 概要 |
|:-----------------|:------------------|:-------------------|
| 部屋番号 | 文字列（1行） | センサーを設定してある部屋番号（場所） |
| 通知先電話番号 | 文字列（1行） | 非感知の場合に通知する電話番号 |
| 最終TwilioCallSid | 文字列（1行） | Twilioで発信したときに取得するTwilio上の発信情報ID |

### **センサー感知状況**

| フィールド名 | 種別 | 概要 |
|:-----------------|:------------------|:-------------------|
| 部屋番号 | 文字列（1行） | センサーを設定してある部屋番号（場所） |
| 登録日時 | 日時 | センサーで感知した日時 |
| センサーシリアル番号 | 文字列（1行） | 感知したセンサーのシリアル番号 |

### **発信記録**

| フィールド名 | 種別 | 概要 |
|:-----------------|:------------------|:-------------------|
| TwilioCallSid | 文字列（1行） | Twilioで発信したときに取得するTwilio上の発信情報ID |
| ステータス | 文字列（1行） | 発信した通話のステータス |
| 通知先の電話番号 | 文字列（1行） | 発信した電話番号 |
| 発信日時 | 日時 | 発信した日時 |

## **MESHから人感センサーの状態をkintoneへ送信/登録（タイマーで定期的に実行）**
MESHからkintoneへデータを送信するためにカスタムタグを準備。
JavaScriptのajax通信を利用して5分置きに人感センサーの感知状態をkintoneへ送信する。
[MESH SDK マニュアル](https://meshprj.com/sdk/doc/ja/)
[MESHを使ってドアの開閉をkintoneに通知しよう](https://developer.cybozu.io/hc/ja/articles/213430966-MESH%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%89%E3%82%A2%E3%81%AE%E9%96%8B%E9%96%89%E3%82%92kintone%E3%81%AB%E9%80%9A%E7%9F%A5%E3%81%97%E3%82%88%E3%81%86)

> propertiesについて
> MESHのカスタムタグはJavaScriptから参照可能な値をタグ毎に設定可能

| 参照名 | 概要 |
|:-----------------|:-------------------|
| domain | kintoneのサブドメイン（xxxxx.cybozu.com の xxxxx部） |
| appId | 前項で作成したセンサー感知状況アプリのID |
| apiToken | 上記アプリのAPIトークン |
| roomNo | センサーを設置している部屋番号 |
| serialNo | センサーのシリアル番号 |

```mesh_kintone.js
var url = "https://" + properties.domain + "/k/v1/record.json";
var appId = properties.appId;
var apiToken = properties.apiToken;
var room_no = properties.roomNo;
var serial_no = properties.serialNo;

var data = {
	"app": appId,
	"record": {
		"room_no": {
			"value": room_no
		},
		"serial_no": {
			"value": serial_no
		}
	}
};

ajax ({
	url : url,
	data : JSON.stringify(data),
	type : "POST",
	contentType : "application/json",
	dataType : "json",
	timeout : 5000,
	headers: {
		"X-Cybozu-API-Token": apiToken
	},
	success : function ( contents ) {
		log("success");
	},
	error : function ( request, errorMessage ) {
		log("error");
		log(request.responseText);
		log(errorMessage);
	}
});

return {
	resultType : "pause"
};
```

## **MESHからAWSへ配置しておいたPHPをコール（タイマーで定期的に実行）**
MESHからセンサー3台の感知状態をチェックするためのカスタムタグを準備。
JavaScriptのajax通信を利用して1分置きに感知状態を確認するためのPHPをキックする。

| 参照名 | 概要 |
|:-----------------|:-------------------|
| domain | 感知状態を確認するためのPHPのURL |

```mesh_twilio_call.js
var url = properties.domain;
var data = {
	"dummy": ""
};

ajax ({
    url : url,
    data : JSON.stringify(data),
	type : "POST",
    contentType : "application/json",
    dataType : "json",
    timeout : 5000,
	success : function ( contents ) {
        log("success");
        log(contents);
    },
    error : function ( request, errorMessage ) {
        log("error");
		log(url);
		log(request.responseText);
		log(errorMessage);
    }
});
 
return {
    resultType : "pause"
};
```

## **PHPにて人感センサーの状態を確認し、3台とも非感知であればtwilioで電話発信**
センサーの感知状態をチェックするためのPHP。
上記mesh_twilio_call.jsから呼び出して、kintoneのデータを取得および3台とも非感知であれば
twilioから電話発信する。

> KintoneManager.php, TwilioManager.phpについて
> 独自ライブラリ。kintoneとのデータのやり取り、Twilioとのデータのやり取りを
> ライブラリ化したもの、そこそこの行数があるのでここでは割愛

```twilio_call.php
<?php

// 各種ライブラリの読み込み
require_once 'lib/KintoneManager.php';
require_once 'lib/TwilioManager.php';


// 部屋番号
$postRoomNo = '';

// 通話内容
$talkContents = '<Response>' . "\n"
			  . '<Say language="ja-JP" voice="alice" loop="3">'
			  . $postRoomNo . 'ごうしつのじんかんせんさーがひかんちになりました。'
			  . '</Say>' . "\n"
			  . '</Response>' . "\n";

// 部屋番号のPOST送信を取得
if (isset($_POST['room_no'])) {
	$postRoomNo = htmlspecialchars($_POST['room_no'], ENT_QUOTES, 'UTF-8');
}

// 部屋番号がなければ処理しない
if ($postRoomNo != '') {
	
	// インスタンス生成
	$km = new KintoneManager();
	$tm = new TwilioManager();
	
	// 部屋番号から設置してあるセンサー情報を取得
	$retSetupSensor = $km->getSetupSensor($postRoomNo);
	if (is_null($retSetupSensor) || !is_array($retSetupSensor)) {
		exit;
	}
	
	// 部屋番号から通知先設定の情報を取得
	$retPushMaster = $km->getPushMaster($postRoomNo);
	if (is_null($retPushMaster)) {
		exit;
	}
	
	// 対象の部屋のセンサー状況を取得
	$retSensorStatus = $km->getSensorStatusWithinMinute($postRoomNo, $retPushMaster->within_minute);
	if (is_null($retSensorStatus) || !is_array($retSensorStatus)) {
		exit;
	}
	
	// センサー状況と設置してあるセンサーの台数が一致するかどうか判定
	$sensorList = NULL;
	$sensorCount = 0;
	
	for ($i=0; $i<count($retSensorStatus); $i++) {
		if (is_null($sensorList)) {
			// リストが未登録の場合は登録
			$sensorList[$sensorCount] = $retSensorStatus[$i]->serial_no;
			$sensorCount++;
		}
		else if (!in_array($retSensorStatus[$i]->serial_no, $sensorList)){
			// 同じシリアル番号がリストになければ登録
			$sensorList[$sensorCount] = $retSensorStatus[$i]->serial_no;
			$sensorCount++;
		}
	}
	
	if (count($retSetupSensor) != $sensorCount) {
		exit;
	}
	
	// 以下、電話発信処理へ
	
	// 前回のTwilioCallSidありの場合
	if (!is_null($retPushMaster->last_call_sid) && $retPushMaster->last_call_sid != '') {
	
		// Twilioから前回のステータスを取得
		$lastCallData = $tm->GetCallData($retPushMaster->last_call_sid);
		
		// 対象となる通話記録のレコードIDを取得
		$retCallHistory = $km->getCallHistory($retPushMaster->last_call_sid);
		
		// 通話記録へステータスをセット
		if (!is_null($lastCallData) && !is_null($lastCallData->status) &&
			!is_null($retCallHistory) && !is_null($retCallHistory->id)) {
			
			$ret_msg = $km->setCallHistoryStatus($retCallHistory->id, $lastCallData->status);
		}
		
		// 通話記録が通知先設定の非感知で通話するまでの分数より過去の場合
		date_default_timezone_set('Europe/London');	// UTCで比較するためタイムゾーンをロンドンに
		$prevDate = $lastCallData->startTime->getTimestamp();
		$nowDate = strtotime('-' . $retPushMaster->within_minute . ' minute');
		if ($prevDate < $nowDate) {
			
			// コール
			$ret_sid = $tm->CallTel($retPushMaster->push_tel_no, $talkContents);
			
			// 通知先設定アプリの対象レコードへTwilioCallSidをセット
			if (!is_null($ret_sid)) {
				$ret_msg = $km->setPushMasterCallSid($retPushMaster->id, $ret_sid);
				
				// 通話記録へレコード登録
				$km->insertCallHistory($ret_sid, 1, $retPushMaster->push_tel_no);
				
				// Twilioから電話発信ステータスを取得
				$lastCallData = $tm->GetCallData($ret_sid);
				
				// 対象となる通話記録のレコードIDを取得
				$retCallHistory = $km->getCallHistory($ret_sid);
				
				// 通話記録へステータスをセット
				if (!is_null($lastCallData) && !is_null($lastCallData->status) &&
					!is_null($retCallHistory) && !is_null($retCallHistory->id)) {
					
					$ret_msg = $km->setCallHistoryStatus($retCallHistory->id, $lastCallData->status);
				}
			}
		}
		else {
			
			// Twilioから24時間以内の前回コール情報を取得
			$retCallHistory24 = $km->getCallHistoryWithin24Hours($retPushMaster->last_call_sid);
			if (!is_null($retCallHistory24) && !is_null($retCallHistory24->retry_num) && !is_null($retCallHistory24->status)) {
				
				// ステータスが未完了かつ再試行回数以下かどうか
				if ($retCallHistory24->status != TwilioManager::CALL_ST_COMPLETED && $retPushMaster->retry_num > $retCallHistory24->retry_num) {
					
					// コール
					$ret_sid = $tm->CallTel($retPushMaster->push_tel_no, $talkContents);
					
					// 通知先設定アプリの対象レコードへTwilioCallSidをセット
					if (!is_null($ret_sid)) {
						$ret_msg = $km->setPushMasterCallSid($retPushMaster->id, $ret_sid);
						
						// 通話記録へレコード登録
						$km->insertCallHistory($ret_sid, ($retCallHistory24->retry_num + 1), $retPushMaster->push_tel_no);
						
						// Twilioから電話発信ステータスを取得
						$lastCallData = $tm->GetCallData($ret_sid);
						
						// 対象となる通話記録のレコードIDを取得
						$retCallHistory = $km->getCallHistory($ret_sid);
						
						// 通話記録へステータスをセット
						if (!is_null($lastCallData) && !is_null($lastCallData->status) &&
							!is_null($retCallHistory) && !is_null($retCallHistory->id)) {
							
							$ret_msg = $km->setCallHistoryStatus($retCallHistory->id, $lastCallData->status);
						}
					}
				}
			}
		}
	}
	// 前回のTwilioCallSidなしの場合
	else {
		// コール
		$ret_sid = $tm->CallTel($retPushMaster->push_tel_no, $talkContents);
		
		// 通知先設定アプリの対象レコードへTwilioCallSidをセット
		if (!is_null($ret_sid)) {
			$ret_msg = $km->setPushMasterCallSid($retPushMaster->id, $ret_sid);
			
			// 通話記録へレコード登録
			$km->insertCallHistory($ret_sid, 1, $retPushMaster->push_tel_no);
			
			// Twilioから電話発信ステータスを取得
			$lastCallData = $tm->GetCallData($ret_sid);
			
			// 対象となる通話記録のレコードIDを取得
			$retCallHistory = $km->getCallHistory($ret_sid);
			
			// 通話記録へステータスをセット
			if (!is_null($lastCallData) && !is_null($lastCallData->status) &&
				!is_null($retCallHistory) && !is_null($retCallHistory->id)) {
				
				$ret_msg = $km->setCallHistoryStatus($retCallHistory->id, $lastCallData->status);
			}
		}
	}
}
?>
```