【つくった】心拍数お知らせページ【Withings】

ぃっす(お疲れ様です)。なるさわです。

0.こないだつくったもの

先日こんなもん作りました↓

簡単に言うと、「24時間自宅の寝室or仕事部屋前を通過しなかったらツイッターなどのみんなに教えるシステム」です。死んでるかも的な。

このシステムの問題点の一つに、
ただ出かけているだけなのに通知がされてしまい、変な心配をかけてしまう
というものがあります。

実際、


1日家を空けて出かけたらこんな感じで通知されました。(この時はテストも兼ねていましたが。)

一応死んでないことをツイートしましたが、これ
自動ツイートされてることに気づかずに、旅を2か月ぐらいエンジョイしていたらインターネット上では死んだことにされね?

ということに気づいてしまいました。これは大変。

そういうことで、出先にいても常に誰かが僕の生死を確認できるようにしてやろうじゃないかと思い立ちました。
心拍数で。

1.こんな感じに作りたい

  1. スマートウォッチで計測

  2. スマホとスマートウォッチのデータを同期させる

  3. スマホがwithings(スマートウォッチの会社)のサーバーと同期してくれる

  4. ↑のデータをラズパイで拾って整形する

  5. Googleスプレッドシートに書き込む

  6. スプレッドシートが埋め込まれたWebサイトが更新される

上記のようにいたしました

ここから難しい話が始まるので、
結果どうなっているかは9章まで飛ばしてください!

2.使ったもの

  • ラズパイ(生存確認システム構築時に購入)

  • Withings製 Steel HR (スマートウォッチ)

  • 公開済みwebサイト(AWSで静的ホスティングしてるだけ)

  • Googleスプレッドシート

以上です。簡単ですね

3.取得しよう 心拍数

WithingsのスマートウォッチはAPIが公開されているため、
Developerかなんかに登録したら
取得したデータを結構詳細に頂くことができます。

一応開発向けでないページでも確認することができるのですが、
30分おきの数値しか参照できません。(スマホアプリでも同様)

Withingsのページで見た心拍数

「csvでくれ」ってのを↑のページでリクエストすればもらえますが、
メールでリンクが送られてくるのでいちいちそれをどうこうしていたらキリがありません。API使いましょう。

WithingsのDeveloper Portalに行くと登録を促されますので、チャチャっと登録します。↓のサイトが参考になると思います。弄ってる日本人が少ない。

これ、APIを叩くためにaccess_tokenってのを取得しないといけないのですが、まあ大変でした。
30秒で
「URLからコード拾って別のURLを作ってaccess_tokenもらいに行く」
ここまでしないといけないので。
何回か失敗して本当に悩みましたが、自分がノロマなだけでした。悲しい

そうしてaccess_tokenをもらえたところで
心拍数、頂いちゃおうぜ!

公式ドキュメントを参考に、以下のcurl実行。

curl --header "Authorization: Bearer YOUR_ACCESS_TOKEN" --data "action=getintradayactivity&data_fields=heart_rate" 'https://wbsapi.withings.net/v2/measure  '

参照するデータの開始時刻(startdate)と終了時刻(enddate)も指定できるのですが、指定なしだと

If no startdate and enddate are passed as parameters, the most recent activity data will be returned.

Withings API Referenceより

とのことなので、直近のアレを返してくれるらしいです。期待。

返ってきたのが以下。

{
  "status": 0,
  "body": {
    "series": {
    }
  }
}

え?僕死んでます?

まあいろいろ試した結果わかったのが、
心拍数は開始時刻(startdate)と終了時刻(enddate)を指定しないと正しく返ってこない
ってことっぽいです。公式の嘘つき~~~~!!!!!

ちなみに歩数は時刻指定しなくても正しく取得できるみたいです。

さて、開始と終了時刻を指定して取得したものが以下です

{
  "status": 0,
  "body": {
    "series": {
      "1657811150": {
        "heart_rate": 80,
        "model": "Activite Steel HR",
        "model_id": 55,
        "deviceid": "himitsu"
      },
      "1657811723": {
        "heart_rate": 69,
        "model": "Activite Steel HR",
        "model_id": 55,
        "deviceid": "himitsu"
      },
      "1657812298": {
        "heart_rate": 64,
        "model": "Activite Steel HR",
        "model_id": 55,
        "deviceid": "himitsu"
      },
~~~~~~中略~~~~~~
      "1657886159": {
        "heart_rate": 93,
        "model": "Activite Steel HR",
        "model_id": 55,
        "deviceid": "himitsu"
      },
      "1657886728": {
        "heart_rate": 74,
        "model": "Activite Steel HR",
        "model_id": 55,
        "deviceid": "himitsu"
      },
      "1657887323": {
        "heart_rate": 86,
        "model": "Activite Steel HR",
        "model_id": 55,
        "deviceid": "himitsu"
      }
    }
  }
}

取得日時(UNIX時刻)と、心拍数と、モデルとモデルIDとデバイスIDです。
デバイスIDはユニークっぽいのでマスクしてます。

非ワークアウト時は10分間隔(10分間の平均なのか10分おきに何秒か計測しているかは不明)、ワークアウト時は1秒おきの数値が記録されているみたいです。
ジム行った日なんかはこれめちゃくちゃ長くなります。(ジムで1時間強運動した日のファイルは30万文字こえてました)

最新の値しか必要ないので、さっき取得した例のやつだと
一番下の

      "1657887323": {
        "heart_rate": 86,
        "model": "Activite Steel HR",
        "model_id": 55,
        "deviceid": "himitsu"

こいつだけ取り出したいですね。
何なら時刻(1657887323)と心拍数(86)以外要らない。

↑のサイトが参考になりそうでしたが、僕うまくできませんでした。ションボリジャンボリー

どうやったかはあとで。たぶん。書いてるうちに忘れてたらすいません。

4.更新しよう token

APIを叩くために最初にもらったaccess_tokenですが、

An access_token expires after 3 hours.

A refresh_token expires after a year.

When you request new access_token and refresh_token, the former refresh_token stops being valid after 8 hours, or as soon as the new access_token is used. This is a safety net in case you were not able to store the new access_token and refresh_token after requesting them.

Withings Public API integration guideより

access_tokenは3時間有効で、access_tokenを発行したときに一緒にもらったrefresh_tokenを使って更新しないといけないみたいですね。3時間おきて。めんどくさ。

curl --data "action=requesttoken&grant_type=refresh_token&client_id={client_id}&client_secret={client_secret}&refresh_token={今のrefresh_token}" 'https://wbsapi.withings.net/v2/oauth2'

↑のcurlを叩くと新しいaccess_token(とrefresh_token)が以下の形で返ってきます。

{
  "status": 0,
  "body": {
    "userid": 12345678,
    "access_token": "access_tokenがここに入る",
    "refresh_token": "refresh_tokenがここに入る",
    "scope": "user.metrics,user.activity",
    "expires_in": 10800,
    "token_type": "Bearer"
  }
}

ここから"access_token"と"refresh_token"を取り出して、次回から叩くAPIのcurlコマンドに組み込んでいくことになります。めんどっちいな。

そういうわけで、各種トークンを更新するシェルスクリプトを作りました。

#!/bin/bash

#前提としてtoken.jsonに前回refreshした時のレスポンスがjqで保存されている
# access_tokenの格納
access_token=`tail token.json -n 7 | head -n 1 | tr -d , | sed 's/    "access_token": "//' | sed 's/"//'`

# refresh_tokenの格納
refresh_token=`tail token.json -n 6 | head -n 1 | tr -d , | sed 's/    "refresh_token": "//' | sed 's/"//'`

#token再取得実行
curl --data "action=requesttoken&grant_type=refresh_token&client_id={client_idを入れる}&client_secret={client_secretを入れる}&refresh_token=$refresh_token" 'https://wbsapi.withings.net/v2/oauth2' | jq > token.json

sleep 5

# access_tokenの格納
access_token=`tail token.json -n 7 | head -n 1 | tr -d , | sed 's/    "access_token": "//' | sed 's/"//'`

# refresh_tokenの格納
refresh_token=`tail token.json -n 6 | head -n 1 | tr -d , | sed 's/    "refresh_token": "//' | sed 's/"//'`

# access_tokenを環境変数へ
export access_token

# refresh_tokenを環境変数へ
export refresh_token

echo "new access_token is $access_token"
echo "new refresh_token is $refresh_token"

こいつが"refresh_token.sh"だとしたら、
"source refresh_token.sh"とか叩けば変数を保持したまま実行できます。

次からAPIを叩くときは、access_tokenを入れるべきところに$access_tokenとか入れればよくなるわけです。便利。

5.勝手に取得しよう 心拍数 勝手に更新しよう token

ここまでにした動作を定期的に勝手にやってしまいたいわけです。

  1. tokenの更新(前回更新から3時間経たないぐらいで)

  2. 心拍数の取得(10分おき)

ごちゃごちゃやった結果こうなりました。
エンジニアの皆様については、一旦目をつむってめちゃめちゃスクロールしてください。

#!/bin/bash

token_last_time=`cat time`
export token_last_time

expect_refresh_time=`expr $token_last_time + 9000` 
export expect_refresh_time

now_unix=`date +%s`
export now_unix

diff_time=`expr $expect_refresh_time - $now_unix`
export diff_time

#token残り2時間半切ったら有効期限延長
if [ $diff_time -le 0 ]; then
    # access_tokenの格納
    access_token=`tail token.json -n 7 | head -n 1 | tr -d , | sed 's/    "access_token": "//' | sed 's/"//'`

    # refresh_tokenの格納
    refresh_token=`tail token.json -n 6 | head -n 1 | tr -d , | sed 's/    "refresh_token": "//' | sed 's/"//'`

    #token再取得実行
    curl --data "action=requesttoken&grant_type=refresh_token&client_id={client_id}&client_secret={client_secret}&refresh_token=$refresh_token" 'https://wbsapi.withings.net/v2/oauth2' | jq > token.json

    sleep 5

    # access_tokenの格納
    access_token=`tail token.json -n 7 | head -n 1 | tr -d , | sed 's/    "access_token": "//' | sed 's/"//'`

    # refresh_tokenの格納
    refresh_token=`tail token.json -n 6 | head -n 1 | tr -d , | sed 's/    "refresh_token": "//' | sed 's/"//'`

    # access_tokenを環境変数へ
    export access_token

    # refresh_tokenを環境変数へ
    export refresh_token

    echo "new access_token is $access_token"
    echo "new refresh_token is $refresh_token"

    date +%s > time
else
    access_token=`tail token.json -n 7 | head -n 1 | tr -d , | sed 's/    "access_token": "//' | sed 's/"//'`
    refresh_token=`tail token.json -n 6 | head -n 1 | tr -d , | sed 's/    "refresh_token": "//' | sed 's/"//'`

    export access_token
    export refresh_token
fi

#各UNIX時刻の格納
today_UNIX=`date -d "00:00:00" +'%s'`
export today_UNIX

#yesterday_UNIX=`date -d "yesterday 00:00:00" +'%s'`
#export yesterday_UNIX

tomorrow_UNIX=`date -d "tomorrow 00:00:00" +'%s'`
export tomorrow_UNIX

#時刻の取得
now_hour=`date +%-H`

#現在保存されている正常なHR取得
old_hr=`cat bpm`

#現在保存されているものの正常な取得時刻を取得
old_hr_date_unix=`cat unixgettime`

sleep 1

curl --header "Authorization: Bearer $access_token" --data "action=getintradayactivity&startdate=$today_UNIX&enddate=$tommorow_UNIX&data_fields=heart_rate" 'https://wbsapi.withings.net/v2/measure' | jq > hr.json

sleep 5

#直近のHR取得
recent_hr=`tail hr.json -n 8 | head -n 1 | tr -d , | sed 's/        "heart_rate": //'`

#HRが数列ではなかった場合過去データに戻す
if [[ "$recent_hr" =~ ^[0-9]+$ ]]; then
    echo $recent_hr > bpm
else
    recent_hr=$old_hr
    #statusコードが401だった場合token再取得
    status_code=`head hr.json -n 2 | tail -n 1 | tr -d , | sed 's/  "status": //'`
    if [ $status_code -eq 401 ]; then
         # access_tokenの格納
        access_token=`tail token.json -n 7 | head -n 1 | tr -d , | sed 's/    "access_token": "//' | sed 's/"//'`

        # refresh_tokenの格納
        refresh_token=`tail token.json -n 6 | head -n 1 | tr -d , | sed 's/    "refresh_token": "//' | sed 's/"//'`

        #token再取得実行
        curl --data "action=requesttoken&grant_type=refresh_token&client_id={client_id}&client_secret={client_secret}&refresh_token=$refresh_token" 'https://wbsapi.withings.net/v2/oauth2' | jq > token.json

        sleep 7

        # access_tokenの格納
        access_token=`tail token.json -n 7 | head -n 1 | tr -d , | sed 's/    "access_token": "//' | sed 's/"//'`

        # refresh_tokenの格納
        refresh_token=`tail token.json -n 6 | head -n 1 | tr -d , | sed 's/    "refresh_token": "//' | sed 's/"//'`

        # access_tokenを環境変数へ
        export access_token

        # refresh_tokenを環境変数へ
        export refresh_token

        echo "new access_token is $access_token"
        echo "new refresh_token is $refresh_token"

        date +%s > time
    else
        :
    fi
fi

#直近の取得時刻を取得
hr_date_unix=`tail hr.json -n 9 | head -n 1 | sed 's/      "//' | sed 's/": {//'`

#UNIX時刻が数列ではなかった場合過去データに戻す
if [[ "$hr_date_unix" =~ ^[0-9]+$ ]]; then
    echo $hr_date_unix > unixgettime
else
    hr_date_unix=$old_hr_date_unix
fi

#見やすい時間に直す
hr_date=`date -d @$hr_date_unix`

export recent_hr
export hr_date

echo "Recent Heart Rate is  $recent_hr ($hr_date)"

とりあえず以下何してるか!

token_last_time=`cat time`
export token_last_time

expect_refresh_time=`expr $token_last_time + 9000` 
export expect_refresh_time

now_unix=`date +%s`
export now_unix

diff_time=`expr $expect_refresh_time - $now_unix`
export diff_time

"time"ってファイルに前回token取得時刻をUNIX時刻で保存しておいて、
最後に取得した時間をtoken_last_timeに格納、
更新しないといけないUNIX時刻(2時間半後)をexpect_refresh_timeに格納、
現在時刻(UNIX時刻)をnow_unixに格納、
更新予定時刻と現在時刻の差分をdiff_timeに格納しています。
変数の法則性の無さ。exportする意味ないです。たぶん。今後全部。

#token残り2時間半切ったら有効期限延長
if [ $diff_time -le 0 ]; then
    # access_tokenの格納
    access_token=`tail token.json -n 7 | head -n 1 | tr -d , | sed 's/    "access_token": "//' | sed 's/"//'`

    # refresh_tokenの格納
    refresh_token=`tail token.json -n 6 | head -n 1 | tr -d , | sed 's/    "refresh_token": "//' | sed 's/"//'`

    #token再取得実行
    curl --data "action=requesttoken&grant_type=refresh_token&client_id={client_id}&client_secret={client_secret}&refresh_token=$refresh_token" 'https://wbsapi.withings.net/v2/oauth2' | jq > token.json

    sleep 5

    # access_tokenの格納
    access_token=`tail token.json -n 7 | head -n 1 | tr -d , | sed 's/    "access_token": "//' | sed 's/"//'`

    # refresh_tokenの格納
    refresh_token=`tail token.json -n 6 | head -n 1 | tr -d , | sed 's/    "refresh_token": "//' | sed 's/"//'`

    # access_tokenを環境変数へ
    export access_token

    # refresh_tokenを環境変数へ
    export refresh_token

    echo "new access_token is $access_token"
    echo "new refresh_token is $refresh_token"

    date +%s > time
else
    access_token=`tail token.json -n 7 | head -n 1 | tr -d , | sed 's/    "access_token": "//' | sed 's/"//'`
    refresh_token=`tail token.json -n 6 | head -n 1 | tr -d , | sed 's/    "refresh_token": "//' | sed 's/"//'`

    export access_token
    export refresh_token
fi

なんかめんどくさくなってきた。
diff_timeが0以下になったらtokenを再取得しにいってjsonに保存してます。
sleep5は気持ちです
elseで普通にtoken格納。

#各UNIX時刻の格納
today_UNIX=`date -d "00:00:00" +'%s'`
export today_UNIX

#yesterday_UNIX=`date -d "yesterday 00:00:00" +'%s'`
#export yesterday_UNIX

tomorrow_UNIX=`date -d "tomorrow 00:00:00" +'%s'`
export tomorrow_UNIX

#時刻の取得
now_hour=`date +%-H`

#現在保存されている正常なHR取得
old_hr=`cat bpm`

#現在保存されているものの正常な取得時刻を取得
old_hr_date_unix=`cat unixgettime`

sleep 1

curl --header "Authorization: Bearer $access_token" --data "action=getintradayactivity&startdate=$today_UNIX&enddate=$tommorow_UNIX&data_fields=heart_rate" 'https://wbsapi.withings.net/v2/measure' | jq > hr.json

sleep 5

ここから心拍数の取得ですが、
取得範囲の開始時刻に0時のUNIX時刻、
終了時刻に明日の0時のUNIX時刻をしていしないとうまいこと引っ張ってこれません。
それらを変数に格納して取得して、curl実行してます。
あ、実行前に、エラーが起こった際変な数字や記号を心拍数として出力しないように
正常にとれた時の数値をoldとして変数に持っていってます。
sleepは気持ちです。

#直近のHR取得
recent_hr=`tail hr.json -n 8 | head -n 1 | tr -d , | sed 's/        "heart_rate": //'`

#HRが数列ではなかった場合過去データに戻す
if [[ "$recent_hr" =~ ^[0-9]+$ ]]; then
    echo $recent_hr > bpm
else
    recent_hr=$old_hr
    #statusコードが401だった場合token再取得
    status_code=`head hr.json -n 2 | tail -n 1 | tr -d , | sed 's/  "status": //'`
    if [ $status_code -eq 401 ]; then
         # access_tokenの格納
        access_token=`tail token.json -n 7 | head -n 1 | tr -d , | sed 's/    "access_token": "//' | sed 's/"//'`

        # refresh_tokenの格納
        refresh_token=`tail token.json -n 6 | head -n 1 | tr -d , | sed 's/    "refresh_token": "//' | sed 's/"//'`

        #token再取得実行
        curl --data "action=requesttoken&grant_type=refresh_token&client_id={client_id}&client_secret={client_secret}&refresh_token=$refresh_token" 'https://wbsapi.withings.net/v2/oauth2' | jq > token.json

        sleep 7

        # access_tokenの格納
        access_token=`tail token.json -n 7 | head -n 1 | tr -d , | sed 's/    "access_token": "//' | sed 's/"//'`

        # refresh_tokenの格納
        refresh_token=`tail token.json -n 6 | head -n 1 | tr -d , | sed 's/    "refresh_token": "//' | sed 's/"//'`

        # access_tokenを環境変数へ
        export access_token

        # refresh_tokenを環境変数へ
        export refresh_token

        echo "new access_token is $access_token"
        echo "new refresh_token is $refresh_token"

        date +%s > time
    else
        :
    fi
fi

#直近の取得時刻を取得
hr_date_unix=`tail hr.json -n 9 | head -n 1 | sed 's/      "//' | sed 's/": {//'`

#UNIX時刻が数列ではなかった場合過去データに戻す
if [[ "$hr_date_unix" =~ ^[0-9]+$ ]]; then
    echo $hr_date_unix > unixgettime
else
    hr_date_unix=$old_hr_date_unix
fi

#見やすい時間に直す
hr_date=`date -d @$hr_date_unix`

export recent_hr
export hr_date

echo "Recent Heart Rate is  $recent_hr ($hr_date)"

直近の心拍数の取得ですが、tailとheadとtrとsedを組み合わせて、
「curlレスポンスの下から8行切り出す」→「先頭行だけ切り出す」→「,」消す→「 "heart_rate":」を消す
とめちゃめちゃヘンテコなことして最新の心拍数の数値だけ取得して変数に格納しています。
格納したものが数列だった場合は、正常に取れた数値として「bpm」というファイルに保存し、
そうでない場合かつレスポンスのステータスコードが401(token期限切れ)だった場合はtokenの再取得をするようにしています。
※テスト中になぜか一回期限切れになってしまったため
ここで当日の値がまだとれていない(時計 - スマホ間未同期)だった場合に前日最後の値が表示されるようにしてます。
心拍数の取得時刻も同じように切り出して、数列判定をして、
数列じゃなかった場合は過去データに戻すようにしています。数列だったら「unixgettime」ってファイルに格納。
最後は見やすい時刻表記を用意して、画面に取得結果を表示です。

出来た!!とりあえず!
このスクリプトを10分おきに流せばとりあえず心拍数の取得はできます。

6.更新しよう Googleスプレッドシート

ここまで取得した心拍数とその取得時刻をスプレッドシートに格納したいわけでございます。

こちらを参考にcurlで投稿できるようにしました。
ほぼ↑のサイト通りです。

参考サイト通りに行うと最後の行にどんどん追記されてしまうので、
追記前に2行目(現在心拍数と時刻が入っている行)を消してから実行する
ようにしました。
以下スプレッドシートに紐づいてるGoogle Apps Script(GAS)の中身です。

// POSTリクエストに対する処理
function doPost(e) {
  // JSONをパース
  if (e == null || e.postData == null || e.postData.contents == null) {
    return;
  }
  var requestJSON = e.postData.contents;
  var requestObj = JSON.parse(requestJSON);

  //  
  // 結果をスプレッドシートに追記
  //

  var ss = SpreadsheetApp.getActive()
  var sheet = ss.getActiveSheet();

  // ヘッダ行を取得
  var headers = sheet.getRange(1,1,1,sheet.getLastColumn()).getValues()[0];
  
  // 2行目を削除
  sheet.deleteRow(2)

  // ヘッダに対応するデータを取得
  var values = [];
  for (i in headers){
    var header = headers[i];
    var val = "";
    switch(header) {
      default:
        val = requestObj[header];
        break;
    }
    values.push(val);
  }
  
  // 行を追加
  sheet.appendRow(values);
}

あとは心拍数取得&token更新実行スクリプトの最後の行に

#スプシに投稿
curl -L -X POST -d "{\"心拍数\":$recent_hr,\"計測時刻\":\"$hr_date\"}" https://script.google.com/macros/s/hogehogehogehogehogehogehoge/exec -o /dev/null -s

を追加して終わり!
-oで結果を次元の狭間に飛ばしているのは、レスポンス文がかなり鬱陶しいからです(毎回エラー返してきやがる)

実行したら

こんな感じで更新してくれます

7.実行しよう 10分おきに

systemdを利用して10分おきに実行してくれるように設定します。

/etc/systemd/system/
内に"HRmonitoring.service"を作成して以下。

[Unit]
Description=HR_monitoring
After=network-online.target nss-lookup.target
Wants=network-online.target

[Service]
ExecStart=/usr/bin/bash -c 'source /hogehoge/hogehoge/hoge_hoge/get_HR.sh'
WorkingDirectory=/hogehoge/hogehoge/hoge_hoge

[Install]
WantedBy=multi-user.target

sourceで実行する必要ない気がするけどうまく動かないのが嫌なのでそのままで…

10分おきに実行するために、/etc/systemd/system/
内に"HRmonitoring.timer"を作成して以下。

[Unit]
Description=HR_monitoring

[Timer]
OnUnitActiveSec=10m

[Install]
WantedBy=timers.target

timerって便利ですね。これだけでいいらしいです。

あとは
sudo systemctl enable HRmonitoring.timer
sudo systemctl start HRmonitoring.timer
sudo systemctl enable HRmonitoring.service
sudo systemctl start HRmonitoring.service

完了!!!

8.埋め込もう Webサイトに

あとはwebサイトにスプレッドシートを埋め込むだけです。

僕のwebサイトどこかに埋め込みました。
なんかセンシティブな情報な気がするので見つけづらいかもしれないです。

9.こんなものができました

  • スマートウォッチで心拍数を取得

  • 10分おきにサーバーに↑の数字を拾いに行く

  • ↑の数字をスプレッドシートに格納

  • ↑のスプレッドシートをWebサイトに埋め込んで、みんなに見えるようにした

  • 万が一生存確認システムが僕を24時間感知していなくても、Webサイトを確認したら生死が一目瞭然!

10.課題

  • スマホでアプリを起動しないと心拍数の同期がされないため、数時間ラグが生まれることが普通になってしまっている(どうしようもない気がする)(ほかにもワークアウト終了時なども同期される?)

  • 一度だけ発生したtokenの自動更新失敗の原因がわからない

  • こんなもの作っても見る人がいない

いい勉強にはなりました。

夜に心拍数が上がってても触れないでね♡

セブンのハッシュポテト代になる