Open13

Google Fit APIで体重情報を取得・登録してみる

Yuma ItoYuma Ito

プロジェクトの作成

Google Fit APIを利用するためには、まずはGoogle Cloud Platformにてプロジェクトを作成する必要がある。

https://cloud.google.com/resource-manager/docs/creating-managing-projects

OAuth2.0による認可

Google Fit APIはOAuth2.0による認可のみ対応しているとのこと。

https://developers.google.com/identity/protocols/oauth2

  1. Google Developer Consoleでメニューから「APIとサービス>OAuth同意画面」にて同意画面を作成
  2. 「APIとサービス>認証情報」画面にて、「認証情報を作成」を押す
  3. OAuthクライアントIDを選択
  4. アプリケーションの種類は「ウェブアプリケーション」を選択
  5. 作成が完了すると、Client IDやシークレットキーなどの情報が生成されるのでメモするか、jsonファイルをダウンロードする

OAuth2.0で指定するスコープは以下のページで確認できる。
https://developers.google.com/identity/protocols/oauth2/scopes

Yuma ItoYuma Ito

リフレッシュトークンは最初に承認した際のみ取得することができるので、注意。
もし、リフレッシュトークンを再取得したい場合は、再承認する必要がある。

API連携の承認を外したい場合は以下のページからアクセス権を削除する
アカウントにアクセスできるアプリ

Yuma ItoYuma Ito

OAuth2Clientオブジェクトの生成

import { Auth, google } from "googleapis";

export async function getOAuth2Client(): Promise<Auth.OAuth2Client> {
  const options = {
    clientId: 'client_id',
    clientSecret: 'client_secret',
    redirectUri: 'redirect_uri',
  };
  const oauth2Client = new google.auth.OAuth2(options);
  oauth2Client.setCredentials({
    refresh_token: 'refresh_token',
  });
  return oauth2Client;
}

OAuth2.0クライアントを作成したときに取得したclient_id, client_secret, redirect_uriを設定してOAuth2オブジェクトを生成する。
setCredentialsには、認証によって取得したアクセストークンやリフレッシュトークンを設定する。リフレッシュトークンだけの場合、API呼び出し時にアクセストークンを自動的に取得してくれる。

Yuma ItoYuma Ito

Googleアカウントの名前を取得してみる

People APIを使うことで名前や連絡先の情報を取得することができる。

https://developers.google.com/people?hl=en

必要なスコープはprofileのみ。
OAuth 2.0 Scopes for Google APIs  |  Google Identity  |  Google Developers

import { google } from 'googleapis';

const oauth2Client = await getOAuth2ClientForLocal();

const peopleApi = google.people({ version: "v1", auth: oauth2Client });
const { data: name } = await peopleApi.people.get({
  resourceName: "people/me",
  personFields: "names",
});
console.log(name);

https://developers.google.com/people/api/rest/v1/people/get?hl=en
認証済みユーザーの情報を取得する場合は resourceNamepeople/meを指定する。
任意のGoogleアカウントの情報を取得場合はresourceNamepeople/{アカウントID}を指定する。(アカウントIDはGmailアドレスではない。)

出力結果

{
  resourceName: 'people/{account_id}',
  etag: '******',
  names: [
    {
      metadata: [Object],
      displayName: 'Yuma Ito',
      familyName: 'Ito',
      givenName: 'Yuma',
      displayNameLastFirst: 'Ito, Yuma',
      unstructuredName: 'Yuma Ito'
    }
  ]
}
Yuma ItoYuma Ito

Google Fit APIで体重情報を取得・記録する

https://developers.google.com/fit/overview

まず、Google Fitのアプリから体重を入力しておく。(APIで登録することもできる)


(引用:https://developers.google.com/fit/overview)

この図で左側のMobile AppからGoogle Fitness Storeにデータを保存した状態。
右側のWeb AppからGoogle Fitness Storeにアクセスして、データを取得したい。

いくつかの概念を理解する必要がある。

  • Data Sources: センサーを表現したもの。ハードウェアセンサー、ソフトウェアセンサーのどちらも表している。
  • Data Types: データの種類を表したもの。(例:心拍数、歩数)
  • Data Points: タイムスタンプつきのデータの配列のこと。データソース(センサー)から取得された値など。
  • Datasets: ある特定期間に対する、同じデータソースかつ同じデータタイプのData Pointsの集合。
  • Sessions: ユーザーがフィットネス(ランニング、バイクなど)を行った期間のこと。
Yuma ItoYuma Ito

必要なスコープ

体重情報を取得するために必要なスコープは以下

https://www.googleapis.com/auth/fitness.body.read

Data Sourcesのリストを表示

https://developers.google.com/fit/rest/v1/reference/users/dataSources/list

自分のアカウントのData Sourcesを表示してみる。

import { google, fitness_v1 } from 'googleapis';

const fitnessApi: fitness_v1.Fitness = google.fitness({
  version: "v1",
  auth: oauth2Client,
});
const { data } = await fitnessApi.users.dataSources.list({
  userId: "me",
});

console.log(JSON.stringify(data));

取得結果は

{
  "dataSource": [
    {
      "dataStreamId": "derived:com.google.weight:com.google.android.gms:merge_weight",
      "dataStreamName": "merge_weight",
      "type": "derived",
      "dataType": {
        "name": "com.google.weight",
        "field": [
          {
            "name": "weight",
            "format": "floatPoint"
          }
        ]
      },
      "application": {
        "packageName": "com.google.android.gms"
      },
      "dataQualityStandard": []
    },
    {
      "dataStreamId": "raw:com.google.weight:com.google.android.apps.fitness:user_input",
      "dataStreamName": "user_input",
      "type": "raw",
      "dataType": {
        "name": "com.google.weight",
        "field": [
          {
            "name": "weight",
            "format": "floatPoint"
          }
        ]
      },
      "application": {
        "packageName": "com.google.android.apps.fitness",
        "version": "",
        "detailsUrl": ""
      },
      "dataQualityStandard": []
    }
  ]
}

dataStreamNameがuser_inputのもの(dataStreamIdはraw:com.google.weight:com.google.android.apps.fitness:user_input)がアプリから入力したデータソースのようだ。

Yuma ItoYuma Ito

体重データのDataSetを取得

データソースのID(dataStreamId)が判明したので、体重のData Setを取得しにいこう。

利用するAPIは以下。
https://developers.google.com/fit/rest/v1/reference/users/dataSources/datasets/get

必要なパラメータは以下。

  • dataSourceId: 取得したいデータソースのID。
  • datasetId: 取得したいData Pointsの期間を指定する。形式は${startTime}-${endTime}。ただし、UNIX時間のナノ秒で指定する。
  • userId: 現時点では'me'のみ指定可。
const from = Date.parse("2022-02-01") * 1e6; // ミリ秒→ナノ秒に変換(10^6倍する)
const to = Date.parse("2022-02-28") * 1e6;

const { data } = await fitnessApi.users.dataSources.datasets.get({
  dataSourceId:
    "raw:com.google.weight:com.google.android.apps.fitness:user_input",
  userId: "me",
  datasetId: `${from}-${to}`,
});

console.log(JSON.stringify(data));

これでリクエストすると・・・

{
  "minStartTimeNs": "1643673600000000000",
  "maxEndTimeNs": "1646006400000000000",
  "dataSourceId": "raw:com.google.weight:com.google.android.apps.fitness:user_input",
  "point": [
    {
      "startTimeNanos": "1645526280000000000",
      "endTimeNanos": "1645526280000000000",
      "dataTypeName": "com.google.weight",
      "value": [
        {
          "fpVal": 80,
          "mapVal": []
        }
      ],
      "modifiedTimeMillis": "1645526330679"
    },
    {
      "startTimeNanos": "1645587240000000000",
      "endTimeNanos": "1645587240000000000",
      "dataTypeName": "com.google.weight",
      "value": [
        {
          "fpVal": 80,
          "mapVal": []
        }
      ],
      "modifiedTimeMillis": "1645587264252"
    }
  ]
}

取得成功!! 🎉🎉🎉

※1ミリ秒=1000マイクロ秒=1000000ナノ秒(10^6ナノ秒)なので変換に注意。最初1000倍しかしていなく、期間を誤って指定していたのでデータが取得できなかった。

Yuma ItoYuma Ito

体重データをAPI経由で登録する

取得ができたので今度は登録をしよう。

利用するAPIは以下。
https://developers.google.com/fit/rest/v1/reference/users/dataSources/datasets/patch#try-it

概要欄に

This method does not use patch semantics: the data points provided are merely inserted, with no existing data replaced

と書いてあったけど、既存のデータを置き換えないのならなぜPATCHメソッドでAPIを設計したのだろう。。。普通にPOSTじゃ駄目なのかな?

体重情報を記録するために必要なスコープは以下。

https://www.googleapis.com/auth/fitness.body.write

登録に必要なパラメータの形式は以下。

{
  "minStartTimeNs": long,
  "maxEndTimeNs": long,
  "dataSourceId": string,
  "point": [
    {
      "startTimeNanos": long,
      "endTimeNanos": long,
      "dataTypeName": string,
      "value": [
        {
          "intVal": integer,
          "fpVal": double,
          "stringVal": string,
          "mapVal": [
            {
              "key": string,
              "value": {
                "fpVal": double
              }
            }
          ]
        }
      ],
    }
  ],
}

point.valueの中身はデータタイプによって適宜設定する。

const timeNs = new Date().getTime() * 1e6;
const dataSourceId =
  "raw:com.google.weight:com.google.android.apps.fitness:user_input";
const newDataSets: fitness_v1.Schema$Dataset = {
  dataSourceId,
  maxEndTimeNs: String(timeNs),
  minStartTimeNs: String(timeNs),
  point: [
    {
      startTimeNanos: String(timeNs),
      endTimeNanos: String(timeNs),
      dataTypeName: "com.google.weight",
      value: [{ fpVal: weight }],
    },
  ],
};
const requestParams: fitness_v1.Params$Resource$Users$Datasources$Datasets$Patch =
  {
    userId: "me",
    dataSourceId,
    datasetId: `${timeNs}-${timeNs}`,
    requestBody: newDataSets,
  };

const data = await fitnessApi.users.dataSources.datasets.patch(requestParams);
Yuma ItoYuma Ito

しかし、ここで以下のエラー。

GaxiosError: No permission to modify data for this source.

下記リンクによると、「user_inputのデータソースは予約されていると思われるから自分でデータソースを作成すると良い」とのこと。
ruby - Saving Point to a Google Fitness API (fitness.body.write) - Stack Overflow

データソースの作成

なので、データソースを作成する。

利用するAPIは以下。
https://developers.google.com/fit/rest/v1/reference/users/dataSources/create

deviceパラメータは指定しなくても作成できるらしい。

const newDataSource: fitness_v1.Params$Resource$Users$Datasources$Create = {
  userId: "me",
  requestBody: {
    application: {
      name: "Sample Data Source",
    },
    dataType: {
      field: [
        {
          name: "weight",
          format: "floatPoint",
        },
      ],
      name: "com.google.weight",
    },
    type: "raw",
  },
};
const { data } = await fitnessApi.users.dataSources.create(newDataSource);

console.log(JSON.stringify(data));

以下のようなレスポンスが得られた。

{
    "dataStreamId": "raw:com.google.weight:645092788289",
    "type": "raw",
    "dataType": {
        "name": "com.google.weight",
        "field": [
            {
                "name": "weight",
                "format": "floatPoint"
            }
        ]
    },
    "application": {
        "name": "Sample Data Source"
    },
    "dataQualityStandard": []
}
Yuma ItoYuma Ito

新しくraw:com.google.weight:645092788289というデータソースを作成できたので、このデータソースに対して体重を登録してみる。

"status":200,"statusText":"OK"

登録はできた。

が、Google Fitのアプリには反映されない。

derived:com.google.weight:com.google.android.gms:merge_weightのデータソースから取得すると、自分で登録した体重情報も登録されていることが分かる。

2022-02-22T10:38:00.000Z - 80 kg (from: raw:com.google.weight:com.google.android.apps.fitness:user_input)
2022-02-23T03:34:00.000Z - 80 kg (from: raw:com.google.weight:com.google.android.apps.fitness:user_input)
2022-02-23T08:12:56.302Z - 75.5 kg (from: raw:com.google.weight:545484f4)

このデータソースがアプリに表示されているというわけではないのか。

いや、少しタイムログがあったけどちゃんとアプリに反映された!!