Google Fit APIで体重情報を取得・登録してみる
プロジェクトの作成
Google Fit APIを利用するためには、まずはGoogle Cloud Platformにてプロジェクトを作成する必要がある。
OAuth2.0による認可
Google Fit APIはOAuth2.0による認可のみ対応しているとのこと。
- Google Developer Consoleでメニューから「APIとサービス>OAuth同意画面」にて同意画面を作成
- 「APIとサービス>認証情報」画面にて、「認証情報を作成」を押す
- OAuthクライアントIDを選択
- アプリケーションの種類は「ウェブアプリケーション」を選択
- 作成が完了すると、Client IDやシークレットキーなどの情報が生成されるのでメモするか、jsonファイルをダウンロードする
OAuth2.0で指定するスコープは以下のページで確認できる。
リフレッシュトークンは最初に承認した際のみ取得することができるので、注意。
もし、リフレッシュトークンを再取得したい場合は、再承認する必要がある。
API連携の承認を外したい場合は以下のページからアクセス権を削除する
アカウントにアクセスできるアプリ
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呼び出し時にアクセストークンを自動的に取得してくれる。
Googleアカウントの名前を取得してみる
People APIを使うことで名前や連絡先の情報を取得することができる。
必要なスコープは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);
resourceName
にpeople/me
を指定する。
任意のGoogleアカウントの情報を取得場合はresourceName
にpeople/{アカウントID}
を指定する。(アカウントIDはGmailアドレスではない。)
出力結果
{
resourceName: 'people/{account_id}',
etag: '******',
names: [
{
metadata: [Object],
displayName: 'Yuma Ito',
familyName: 'Ito',
givenName: 'Yuma',
displayNameLastFirst: 'Ito, Yuma',
unstructuredName: 'Yuma Ito'
}
]
}
Google Fit APIで体重情報を取得・記録する
まず、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: ユーザーがフィットネス(ランニング、バイクなど)を行った期間のこと。
必要なスコープ
体重情報を取得するために必要なスコープは以下
https://www.googleapis.com/auth/fitness.body.read
Data Sourcesのリストを表示
自分のアカウントの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
)がアプリから入力したデータソースのようだ。
体重データのDataSetを取得
データソースのID(dataStreamId
)が判明したので、体重のData Setを取得しにいこう。
利用するAPIは以下。
必要なパラメータは以下。
-
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ナノ秒(
体重データをAPI経由で登録する
取得ができたので今度は登録をしよう。
利用するAPIは以下。
概要欄に
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);
しかし、ここで以下のエラー。
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は以下。
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": []
}
新しく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)
このデータソースがアプリに表示されているというわけではないのか。
いや、少しタイムログがあったけどちゃんとアプリに反映された!!
最終的にAlexaスキルと連携してGoogle Fitに記録できるようになりました!