Withings APIを使ってRでWithingsのデータを取得する

普段Withingsの体重計(Withings Body Cardio)と睡眠計(Withings Sleep)を使っているが、このデータはAPIを利用して取得することができる。

Rを使って体重計のデータを取得するところまでやってみたので手順を記しておく。

API利用登録

Withingsのアカウントにログインした上で、https://account.withings.com/partner/add_oauth2にアクセスしてアプリの登録を行う。

項目のうち、コールバックURLについてはhttrを使用する都合上、http://localhost:1410を指定した。

また、「Enable restricted mode ?」はYesを選択した。コールバックURLには本来は要件があるが(https://developer.withings.com/developer-guide/v3/glossary/glossary-page/#callback-url)、これを満たさない場合に有効にするオプションらしい。接続ユーザー数の制限などがかかるらしいが、おそらく個人的に利用する分には有効でも問題ないだろう。

残りの項目は適当に入力した(全ての項目は後から変更できる)。

パッケージ

APIを叩くのにはhttrを使用した。また、取得したデータのハンドリングのためdplyrtidyrrlisttibbleを使用した。インストールしていなければインストールして読み込んでおく。

#install.packages(c("httr", "dplyr", "tidyr", "rlist", "tibble"))
library(httr)
library(dplyr)
library(tidyr)
library(rlist)
library(tibble)

認証

適宜APIリファレンス(https://developer.withings.com/api-reference/)を確認しながら進めていく。

まず、エンドポイントとなるURLを登録する。

withings <- oauth_endpoint(
  authorize = "https://account.withings.com/oauth2_user/authorize2",
  access = "https://wbsapi.withings.net/v2/oauth2"
)

authorizeには認証コード取得APIのエンドポイントを、accessにはアクセストークン取得APIのエンドポイントを登録する。URLは正しいものをAPIリファレンス(https://developer.withings.com/api-reference/#operation/oauth2-authorize)で確認しておいたほうが良い。後者は最近変更になったので、ネットで適当に調べて出てきた情報はそのまま使えない場合がある。

次にアプリを登録する。

withingsapp <- oauth_app("withings",
  key = "クライアントID",
  secret = "コンシューマーシークレット"
)

クライアントIDとコンシューマーシークレットについてはAPI利用登録後の画面から確認できるので、上記コードはその値で書き換えて実行する。簡単に変更できなさそうなので、扱いには注意。

次に認証を行い、アクセストークン(およびリフレッシュトークン)を取得する。

withings_token <- oauth2.0_token(
    withings, 
    withingsapp, 
    scope = "user.metrics", 
    user_params = list("action" = "requesttoken")
)

APIリファレンス(https://developer.withings.com/api-reference/#operation/oauth2-authorize)に記載のパラメータの大半はhttr::oauth2.0_token()がよしなにやってくれるが、actionuser_paramsを通じて渡す必要がある。

scopeは取得するデータに合わせて指定する(https://developer.withings.com/developer-guide/v3/data-api/all-available-health-data/)。複数指定することもできるが、今回はとりあえず体重計のデータを取得するだけなのでuser.metricsを指定した。

上記コードを実行するとwithings_tokenの中にアクセストークンが保存されるので、これを使ってAPIを叩くことになる。

トークンの更新

アクセストークンは3時間で失効するので、時間が経過した場合はリフレッシュトークンを使ってリフレッシュする。本来はwhithings_token$refresh()を呼び出すだけで良いっぽいが、どうもリフレッシュトークンの保存場所が微妙に違うらしく、少し細工をしないと動かなかった。次のコードを2行とも実行するとトークンがリフレッシュされる。

withings_token$credentials$refresh_token <- withings_token$credentials$body$refresh_token
withings_token$refresh()

リフレッシュトークンの有効期限は1年だが、リフレッシュによりリフレッシュトークンも新しいものになる。

データの取得

今回は体重計データを取得したいので、次のAPIを使う。

httr::POST()を使う場合の例は次のようになる。

req <- POST(
  url = "https://wbsapi.withings.net/measure",
  body = list(
    action = "getmeas",
    #meastype = 1,
    category = 1,
    startdate = as.numeric(as.POSIXct(Sys.Date()-3)),
    enddate = as.numeric(as.POSIXct(Sys.Date()))
  ),
  add_headers(
    Authorization = 
      paste("Bearer", withings_token$credentials$body$access_token)
  )
)

必須のパラメータはactionのみで、ほかは省略しても適当に返ってくる。省略時に具体的にどうなるかはよく調べていない(APIリファレンスにも書いてない)。

データの加工

レスポンスからデータを取り出すにはhttr::content()を使う。

res <- content(req)

この時点ではresの中身はリストなので、うまいこと加工していく必要がある。

まず、データはres$body$measuregrpsの中に入っており、次のような構造になっている。

  • measuregrps: 以下の項目(測定グループ)のリストが複数件存在する可能性がある。
    • grpid: 測定グループに対応する一意なID。
    • attrb: 測定がユーザーにどのように紐付いているのかの情報らしい。APIリファレンスには8までの説明があるが中身をみると10とかがあって謎。
    • date: 測定が行われた時刻(UNIX時)。
    • created: 測定が保存された時刻(UNIX時)。試しに取得したデータではdateと同じだった。データの修正とかしてないからかもしれない。
    • ncategory: 測定データが実際の測定値かユーザーの目標値(?)かを示すカテゴリらしい。
    • deviceid: デバイスのID。別のAPIに渡してデバイス情報を取得するのに使えるらしい。
    • hash_deviceid: APIリファレンスに記載はないが、deviceidと同じ値が入っている。謎。
    • measures: 測定値や項目の情報。1つの測定データに複数含まれる可能性がある。例えば体重と体脂肪量を同時測定した場合などは複数になる。
      • value: 測定値。単位はtypeごとに規定のものがあるが、値は全て整数値になっている。小数表現に直すには、10の冪の指数部に後述のunitに入っている値を代入した値を乗ずる。
      • type: 測定のタイプ。種類が多いのでAPIリファレンス参照: https://developer.withings.com/api-reference/#operation/measure-getmeas
      • unit: valueの実際の表現を得るために使う数字。
      • algo: deprecated.
      • fm: deprecated.
      • fw: deprecated
    • comment: deprecated.

若干厄介なのはmeasuresがネストしているあたりだろう。これは次のようにするとうまくデータフレームにできる。

res$body$measuregrps %>% 
  list.map(
    tibble::tibble(
      grpid,
      attrib,
      date,
      value = measures %>%  list.map(value),
      type = measures %>%  list.map(type),
      unit = measures %>%  list.map(unit)
    )
  ) %>% 
  list.stack() %>% 
  unnest(cols = c(value, type, unit))

参考:rlistでリストを整形してデータフレームにする - Qiita

あとは適当に見栄えを整えてやれば良い。

typeの数字に対応する項目は頑張ってなんとかする。次のようなデータフレームを作っておいて結合すると良いだろう。

meastype <- tibble::tribble(
  ~type, ~description,
  1, "Weight (kg)", 
  4, "Height (meter)", 
  5, "Fat Free Mass (kg)",
  6, "Fat Ratio (%)",
  8, "Fat Mass Weight (kg)",
  9, "Diastolic Blood Pressure (mmHg)",
  10, "Systolic Blood Pressure (mmHg)",
  11, "Heart Pulse (bpm) - only for BPM and scale devices",
  12, "Temperature (celsius)",
  54, "SPO2 (%)",
  71, "Body Temperature (celsius)",
  73, "Skin Temperature (celsius)",
  76, "Muscle Mass (kg)",
  77, "Hydration (kg)",
  88, "Bone Mass (kg)",
  91, "Pulse Wave Velocity (m/s)",
  123, "VO2 max is a numerical measurement of your body’s ability to consume oxygen (ml/min/kg).",
  135, "QRS interval duration based on ECG signal",
  136, "PR interval duration based on ECG signal",
  137, "QT interval duration based on ECG signal",
  138, "Corrected QT interval duration based on ECG signal",
  139, "Atrial fibrillation result from PPG"
)

valueの値は前述のようにunitの値で調整する。具体的にはvalue * 10^unitが実際の値となる。

datecreatedの値はUNIX時なので、as.POSIXct(date, origin = "1970-01-01")のようにすると日時表現を得られる。

まとめると、次のようにしてやれば整う。

res$body$measuregrps %>% 
  list.map(
    tibble::tibble(
      grpid,
      attrib,
      date,
      value = measures %>%  list.map(value),
      type = measures %>%  list.map(type),
      unit = measures %>%  list.map(unit)
    )
  ) %>% 
  list.stack() %>% 
  unnest(cols = c(value, type, unit)) %>% 
  mutate(
    date = as.POSIXct(date, origin = "1970-01-01"),
    value = value * 10^unit
  ) %>% 
  left_join(meastype, "type") %>% 
  select(!c(type, unit))

f:id:Rion778:20220215220311p:plain