Node.js で Google Fit のデータを取得する

今年になってから今更ですがドラクエウォーク始めました。

www.dragonquest.jp

通勤しないけど、このおかげでけっこう外歩きするようになってるので、歩数データと体重データの相関取ったりしてみたいなあと思いました。Google Fit って Web アプリは廃止されてて、スマホアプリの UI しかないんですが、REST API でデータを読み書きはできます。そこで REST API で Fit のデータを取得する方法を調べました。

Node.js で書きたいので googleapis の NPM パッケージを使うことに。

www.npmjs.com

API Key もいいんですが、よりセキュアな OAuth2 認証使うため local-auth パッケージもインストール

www.npmjs.com

OAuth2 を使って、Google の People API を呼び出すサンプルがありました。

https://github.com/googleapis/google-api-nodejs-client/blob/master/samples/oauth2.js

ローカル認証のために、localhost:3000 の http server を起動しリダイレクトして、oauth2Client から token を取得しています。 コマンドを叩くたびに、リダイレクト用ページがブラウザで開き、スマホにセキュリティ通知が来るのがうざいですがちゃんと動きます。

Fit の REST API を使うために、Google Cloud Platform のプロジェクトで Fitness API を有効化します。

https://console.cloud.google.com/apis/library/fitness.googleapis.com

認証情報で OAuth 2.0 クライアント ID を作成してダウンロードし oauth2.keys.json として保存して秘密のディレクトリに格納しておきます。

https://console.cloud.google.com/apis/credentials

Fitness REST API の Aggregate の仕様は以下で見れます。

Users.dataset: aggregate  |  Google Fit  |  Google Developers

歩数と体重を読むだけなので OAuth の認可スコープは、以下でよさそうです。

const scopes = [
  'https://www.googleapis.com/auth/fitness.activity.read',
  'https://www.googleapis.com/auth/fitness.body.read'
];

集計の軸は、dataTypeNamedataSourceId で指定します。ここにサンプルがありました。

Read the Daily Step Total  |  Google Fit  |  Google Developers

以下の指定でよいようです。

{
  "dataTypeName": "com.google.step_count.delta",
  "dataSourceId": "derived:com.google.step_count.delta:com.google.android.gms:estimated_steps"
}

Fit のアプリで登録した体重データは以下のように指定すれば取れました。

{
  "dataTypeName": "com.google.weight.summary",
  "dataSourceId": "derived:com.google.weight:com.google.android.gms:merge_weight"
}

ということで、このサンプルの runSample() 関数を書き換えます。

https://github.com/googleapis/google-api-nodejs-client/blob/master/samples/oauth2.js

async function runSample() {
  const res = await fitness.users.dataset.aggregate({
    userId: 'me',
    requestBody: {
      "aggregateBy": [
        {
          "dataTypeName": "com.google.step_count.delta",
          "dataSourceId": "derived:com.google.step_count.delta:com.google.android.gms:estimated_steps"
        },
        {
          "dataTypeName": "com.google.weight.summary",
          "dataSourceId": "derived:com.google.weight:com.google.android.gms:merge_weight"
        },
      ],
      "bucketByTime": { "durationMillis": 86400000 },
      "startTimeMillis": 1624658400000,
      "endTimeMillis": 1624698000000,
    },
  });

  const steps = res.data.bucket[0].dataset[0].point[0];
  console.log(steps.originDataSourceId);
  console.log(formatDate(steps.startTimeNanos));
  console.log(formatDate(steps.endTimeNanos));
  console.log(steps.dataTypeName);
  console.log(steps.value[0].intVal);

  const weight = res.data.bucket[0].dataset[1].point[0];
  console.log(weight.originDataSourceId);
  console.log(formatDate(weight.startTimeNanos));
  console.log(formatDate(weight.endTimeNanos));
  console.log(weight.dataTypeName);
  console.log(weight.value[0].fpVal);
}

function formatDate(timestamp) {
  let date = new Date();
  date.setTime(timestamp / 1000000);
  const params = {
    year: 'numeric', month: 'numeric', day: 'numeric',
    hour: 'numeric', minute: 'numeric', second: 'numeric',
    hour12: false
  };
  return date.toLocaleString("ja", params);
}

リクエストでは、取得したい期間 (startTimeMillis, endTimeMillis)をエポック時間で指定します。bucketByTime は集計に必要な期間を指定します (上記の read-daily-step-total サンプルに合わせてます)。userId は Google のアカウント名とかではなく me を指定します。

レスポンスの data.bucket[0].dataset は配列になっていて、歩数、体重の順に格納されてます。データの時刻はナノ秒単位で入っているので、フォーマットしました。実行すると以下のようにデータが取れます。

raw:com.google.step_count.cumulative:Google:Pixel 3 XL:caa195e4620531ba:Step Counter
2021/6/26 7:25:33
2021/6/26 15:22:25
com.google.step_count.delta
9422
raw:com.google.weight:com.google.android.apps.fitness:user_input
2021/6/26 8:02:26
2021/6/26 8:02:26
com.google.weight.summary
67.0999984741211

時期によっては、前使ってた Nexus 6P や Moto 360 のデータも入っていました。