導入
自分が今寝ているのか否かを他人に伝えることができたら便利なのでは…?
と思い、就寝中であればtwitterの名前欄に絵文字を追加するプログラムを組んでみました。
twitterだけでなく、slack等他のサービスとの連携も可能です。
仕組み
簡単にまとめると、
- Fitbitで取得できる心拍数を元に、起きているか寝ているかを判定
- 過去3日間の就寝中の心拍数を取得し、それらから基準となる心拍数を計算
- 直近の心拍数を取得し、基準を下回れば寝ていると判定する
- 判定を元にtwitterの名前をAPI経由で編集
- 上記スクリプトを10分間隔で自動実行
といった実装になっています。
Fitbitとは
Fitbit公式ホームページ
Fitbit社が製造する活動量計で、歩数や消費カロリー、心拍数、睡眠状態などを記録してくれます。
また、それらをAPI経由で取得することもできます。
製品によって取得できるデータに制限があります。(私はCharge3を使っています)
睡眠状態のログは残念ながらリアルタイムでは取得できない1ため、今回は心拍数を用いて判定を行いました。
それでは以下に具体的な実装を書いていきます。
実装
事前準備
https://dev.fitbit.com/apps
Fitbitの初期設定を済ませたらdeveloperページにログインします。
REGISTER AN APPタブからappを登録し、oauthに必要な情報を入手、それらを用いてアクセストークンを取得しておきます。
注意点として、app登録時のApplication TypeはPersoanlにしておく必要があります。
ある程度リアルタイムに心拍数を取得することができるGet Heart Rate Intraday Time Series エンドポイントにアクセスするために必要な権限となっています。
また、過去3日間の就寝中の心拍数 と 直近の心拍数 それぞれを取得する用のapp計2つを登録しておきます。
過去3日間の就寝中の心拍数
まず、睡眠ログを取得できるAPIを叩き、3日間それぞれの睡眠開始日時と睡眠時間を取得します。
それらを用いて心拍数を取得するAPIを叩くコードが以下です。
/**
* 直近3日間の睡眠時の心拍数(1分間隔)を
* 配列に格納して返す
* @return array
*/
function getHeartrateWhileSleeping()
{
// API呼び出し
$startDate = date("Y-m-d", strtotime("-3 day"));
$endDate = date("Y-m-d", strtotime("-1 day"));
$url_sleep = "https://api.fitbit.com/1.2/user/-/sleep/date/" . $startDate . "/" . $endDate . ".json";
$header = [
'Authorization: Bearer ' . FITBIT_ACCESS_TOKEN_2,
'Content-Type: application/x-www-form-urlencoded',
];
$response = json_decode(callApi($url_sleep, $header), true);
$sleep_data = [];
$heartrate_data = [];
// 睡眠時の心拍数を取得
for ($i = 0; $i < count($response['sleep']); $i++) {
// 寝始めと睡眠時間を抽出
$sleep_data['startTime'][$i] = $response['sleep'][$i]['startTime'];
$sleep_data['timeInBed'][$i] = $response['sleep'][$i]['timeInBed'];
// apiの仕様に合わせて日時フォーマット変換
$sleep_data['startTime'][$i] = date("Y-m-d H:i", strtotime(str_replace('T', ' ', $sleep_data['startTime'][$i])));
list($date, $start_time) = explode(' ', $sleep_data['startTime'][$i]);
$end_time = date("H:i", strtotime("+" . (string)$sleep_data['timeInBed'][$i] . "minute", strtotime($start_time)));
$url_heart = "https://api.fitbit.com/1/user/-/activities/heart/date/" . $date . "/1d/1min/time/" . $start_time . "/" . $end_time . ".json";
$heartrate_data[$i] = json_decode(callApi($url_heart, $header), true);
sleep(1);
}
return $heartrate_data;
}
(callApiは名の通りAPIを叩くための自作関数です)
次に、上で得られたデータから睡眠判定に使う基準値を計算します。
/**
* 心拍数の配列データから基準値を計算して返す
* データがない場合はexit
* @param array $heartrate_data
* @return array
*/
function calcReferenceValue($heartrate_data)
{
$ref = [];
$tmp = [];
$date = [];
$result = [];
$data_count = 0;
// 必要なデータを抽出
for ($i = 0; $i < count($heartrate_data); $i++) {
for ($j = 0; $j < count($heartrate_data[$i]['activities-heart-intraday']['dataset']); $j++) {
$tmp[$i][$j] = $heartrate_data[$i]['activities-heart-intraday']['dataset'][$j]['value'];
$data_count = count($heartrate_data[$i]['activities-heart-intraday']['dataset']);
}
// 第二引数を0〜1で設定
$ref[$i] = Quartile_mod($tmp[$i], 0.8);
$date[$i] = $heartrate_data[$i]['activities-heart'][0]['dateTime'];
}
if ($data_count === 0) {
exit("No heartrate data in last 3 days");
};
$result['start_date'] = $date[count($date) - 1];
$result['end_date'] = $date[0];
$result['reference_value'] = array_sum($ref) / count($ref);
$result['created_at'] = date("Y-m-d H:i:s");
return $result;
}
基準値の計算自体はQuartile_modという関数で行なっているのですが、
https://blog.poettner.de/2011/06/09/simple-statistics-with-php/
こちらのQuartile関数を参考にさせていただきました。
ソートしたデータの位置を0〜1の値で指定して取得できる関数で、デフォルトで0.25刻みの値を渡して四分位数を計算する関数が実装されています。
今回は0〜1の引数を独自に設定できるようにしました。
第三四分位数(0.75)では値が厳しかったため、0.8〜0.9で運用してみています。
基準値が取得できたら、直近の心拍数との比較に利用できるようにjsonで出力しておきます。
$reference_value = calcReferenceValue($heartrate_data);
$reference_value = json_encode($reference_value, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);
file_put_contents('../../output/reference_value.json', $reference_value);
直近の心拍数
直近の心拍数は、現時点から30分前までのデータを1分単位で取得し、平均をとって算出しています。
できればもっと直近のデータで計算をしたいところですが、FitbitのデータがAPIで取得できるようになるまでに10〜20分ほどかかってしまうためやむをえずといった形です。
/**
* 直近の心拍数の平均値を返す
* 直近のデータがなければexit
* @return float
*/
function getLatestHeartrate()
{
// API呼び出し
$startTime = date("H:i", strtotime("-31 minute"));
$endTime = date("H:i", strtotime("-1 minute"));
$url = "https://api.fitbit.com/1/user/-/activities/heart/date/today/1d/1min/time/" . $startTime . "/" . $endTime . ".json";
$header = [
'Authorization: Bearer ' . FITBIT_ACCESS_TOKEN,
'Content-Type: application/x-www-form-urlencoded',
];
$heartrate_data = json_decode(callApi($url, $header), true);
$tmp = [];
// 必要なデータを抽出
for ($i = 0; $i < count($heartrate_data['activities-heart-intraday']['dataset']); $i++) {
$tmp[$i] = $heartrate_data['activities-heart-intraday']['dataset'][$i]['value'];
}
if ($tmp === []) {
exit("No latest heartrate data");
}
$result = array_sum($tmp) / count($tmp);
return $result;
}
後は心拍数の比較をして完了です。
twitterとの連携はTwitterOAuthを利用させていただきました。
use Abraham\TwitterOAuth\TwitterOAuth;
$latest_heartrate = getLatestHeartrate();
// json取得
$json = file_get_contents('../../output/reference_value.json');
$arr = json_decode($json, true);
// twitter oauth
$connection = new TwitterOAuth(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET);
$connection->get("account/verify_credentials");
// latestと基準値を比較し、latestが基準を下回ればtwitterの名前に絵文字を付与
if ($latest_heartrate < $arr['reference_value']) {
$connection->post("account/update_profile", ["name" => "まつり🛌🏻️"]);
echo "sleep. latest:" . $latest_heartrate . ", reference_value:" . $arr['reference_value'];
} else {
$connection->post("account/update_profile", ["name" => "まつり"]);
echo "not sleep. latest:" . $latest_heartrate . ", reference_value:" . $arr['reference_value'];
}
プログラム全体はgithubに置いておきます。
https://github.com/matsuri0828/sleep-judge
現状アクセストークンの有効期限切れに対応していないので近々アップデート予定です。
スクリプトの定期実行は色々やり方があると思います。
今はmacOSのlaunchdで回してしまっていますが、勉強がてらAWSLambda+CloudWatchEventsに移行してみようかなと。
最後に
はじめはslackと連携して「仕事中居眠りしたらチームのslackに通知する」アプリを作ろうかと思いましたが、リアルタイム性に少し難ありだったため今回はtwitterを選びました。
Fitbitで取得できるデータと他のサービスとの連携は色々できて楽しそうなので、今後も触っていこうと思います。
以上、お読みいただきありがとうございました。
参考
-
コミュニティでも要望が上がっているので、いずれ実装されるかもしれません。また、睡眠に関するAPIの仕様を見てみると、2019年8月時点でも簡単なmetaデータが取得できそうな記述があります。 ↩