(画像は過去に入力したデータを全て Google Fit へ入力しなおした様子)
Fit API 全体の概念
単純にグローバルな「体重」に対して値を追加するみたいになっているわけではない。
各アプリケーション(サードパーティ含む)は自分用の「データソース」を作る。これはセンサーに対応する。例えば「体重計 HOGE-001」みたいなデータソース。このとき「体重 (com.google.weight)」とかデータの種類と「浮動小数点」とかデータ型の定義をしておく。
データソースの定義に従って、データソースにデータポイントを追加してく。例えば「体重は 69.95kg」みたいな。
こうしていくと、複数のデータソースから「体重」データがいくつもできることになる。
実は Google Fit の画面から体重を入力すると user_input というデフォルトで存在するデータソースにそのデータは蓄積される。一方で、自分で独自の「体重」のデータソースを作って追記することもできる。これによって、データソースごとに自分のデータにだけ責任を持つという形にすることができる。
これらの「体重」のデータは最終的に derived:com.google.weight:com.google.android.gms:merge_weight というデータソースに集計されて、Fit で表示されている。
あとアクティビティ(ランニング)に対応するセッションとかがあるけど、今回は使ってないので調べてない。
「体重」を記録する場合
なんらかの方法で体重情報を取得できるとして、それを Google Fit に保存したい場合を想定する。
全体の流れは以下の通り
- Fitness API が有効な OAuth の設定をする
- Developer Console で 「Fitness API」を有効にしたプロジェクトをつくり、「OAuth 2.0 クライアント ID」を作成しておく。
- 対応するデータソースを作る (体重計を1つのセンサーとみなして)
- データソースへ値を追記する
Perl での例をしめす。CLI アプリケーションとしての実装。oob でキーを入力するため初回のみインタラクティブな インターフェイスになっている。
use v5.12;
use LWP::Authen::OAuth2;
use Path::Class;
use JSON;
use HTTP::Request::Common qw(GET HEAD POST DELETE PATCH);
use DateTime;
use constant {
CLIENT_ID => '',
CLIENT_SECRET => '',
};
my $token_file = file('.token_file');
my $token_string = eval { $token_file->slurp } || '';
my $google = LWP::Authen::OAuth2->new(
client_id => CLIENT_ID(),
client_secret => CLIENT_SECRET,
service_provider => "Google",
redirect_uri => "urn:ietf:wg:oauth:2.0:oob",
save_tokens => sub {
my ($token_string) = @_;
my $fh = $token_file->openw;
print $fh $token_string;
close $fh;
},
save_tokens_args => [],
token_string => $token_string,
);
unless ($google->token_string) {
# 新規 OAuth 認証
my $uri = $google->authorization_url(scope => join(' ',
'https://www.googleapis.com/auth/fitness.body.read',
'https://www.googleapis.com/auth/fitness.body.write',
));
printf "Access to authorization: %s\n", $uri;
printf "Input authorization code: ";
my $code = <>;
chomp $code;
$google->request_tokens(code => $code);
}
# データソース作成
# 既にある場合は 409 になる
my $res = $google->request(POST "https://www.googleapis.com/fitness/v1/users/me/dataSources",
Content_Type => "application/json;encoding=utf-8",
Content => encode_json({
"application" => {
"name" => "foobar baz",
"detailsUrl" => "http://example.com",
"version" => "1",
},
"dataType" => {
"name" => "com.google.weight",
"field" => [
{
"name" => "weight",
"format" => "floatPoint"
}
]
},
"dataStreamName" => "foobar",
"type" => "raw",
"device" => {
"manufacturer" => "my",
"model" => "foobar",
"type" => "scale",
"uid" => "1000001",
"version" => "1.0"
}
})
);
# 409 の場合エラーメッセージをパースしてデータソースIDを取得している
my $datasourceid = undef;
if ($res->code == 409) {
my $json = decode_json($res->decoded_content);
($datasourceid) = ($json->{error}->{message} =~ /Data Source: ([^ ]+) already exists/);
} elsif ($res->code == 200) {
my $json = decode_json($res->decoded_content);
$datasourceid = $json->{dataStreamId};
} else {
die "failed to request creating data source";
}
unless ($datasourceid) {
die "cannnot retrieve or create datasource";
}
# 送信するデータポイント
my $data_points = [
{ epoch => ..., weight => 69.4 },
{ epoch => ..., weight => 69.4 },
{ epoch => ..., weight => 69.4 },
];
my $minstarttime = min map { $_->{epoch} } @$data_points;
my $maxendtime = max map { $_->{epoch} } @$data_points;
# 追加するリクエストは PATCH
# https://developers.google.com/fit/rest/v1/reference/users/dataSources/datasets/patch
my $datasetid = sprintf("%s-%s", $minstarttime, $maxendtime);
my $res = $google->request(PATCH sprintf("https://www.googleapis.com/fitness/v1/users/me/dataSources/%s/datasets/%s", $datasourceid, $datasetid),
Content_Type => 'application/json;encoding=utf-8',
Content => encode_json({
"dataSourceId" => $datasourceid,
"minStartTimeNs" => $minstarttime * 1000 * 1000 * 1000,
"maxEndTimeNs" => $maxendtime * 1000 * 1000 * 1000,
"point" => [
map {
{
"dataTypeName" => "com.google.weight",
"originDataSourceId" => "",
"startTimeNanos" => $_->{epoch} * 1000 * 1000 * 1000,
"endTimeNanos" => $_->{epoch} * 1000 * 1000 * 1000,
"value" => [
{
"fpVal" => $_->{weight},
}
]
}
} @$data_points
]
})
);
say $res->as_string;
データソースを削除するには
既存のデータポイントが残っていると削除できないため、以下の手順を踏む
- GET dataPointChanges で全てのデータポイントを洗って、startTimeNanos の最小値、endTimeNanos の最大値をもとめる
- DELETE datasets で 求めた startime-endtime を datasetid とする (既存データポイントを削除)
- DELETE dataSources を行う
ただ、データポイントを削除しても deletedDataPoint に入るだけで、完全に消えるわけではない。データソースも、削除は通っても、再度作成を行うと、deletedDataPoint が含まれた古いデータが復活する。ここらへんの挙動はよくわからない。
コード例は以下の通り
my $page_token = "";
my $minstarttime = "inf";
my $maxendtime = 0;
# データポイントを走査してデータ範囲を確定させる
while (1) {
infof("GET dataPointChanges with token %s", $page_token);
my $res = $google->request(GET sprintf("https://www.googleapis.com/fitness/v1/users/me/dataSources/%s/dataPointChanges?%s", $datasourceid, $page_token));
$res->code == 200 or die "failed to get dataPointChanges";
my $json = decode_json($res->decoded_content);
use Data::Dumper;
warn Dumper $json ;
@{ $json->{insertedDataPoint} } or last;
$minstarttime = min $minstarttime, map {
$_->{startTimeNanos}
} @{ $json->{insertedDataPoint} };
$maxendtime = max $maxendtime, map {
$_->{endTimeNanos}
} @{ $json->{insertedDataPoint} };
$page_token = "pageToken=" . $json->{nextPageToken};
}
# 全範囲のデータポイントを削除する
if ($maxendtime) {
my $datasetid = sprintf("%s-%s", $minstarttime, $maxendtime);
infof("Deleting existing data points for this data source %s", $datasetid);
my $res = $google->request(DELETE sprintf("https://www.googleapis.com/fitness/v1/users/me/dataSources/%s/datasets/%s", $datasourceid, $datasetid));
say $res->as_string;
} else {
infof("There are no data point");
}
# データソースを削除する
infof("Deleting this datasource");
my $res = $google->request(DELETE sprintf("https://www.googleapis.com/fitness/v1/users/me/dataSources/%s", $datasourceid));
say $res->as_string;
OMRON の Wi-FI 体重計
突然話は変わるがOMRON の Wi-FI 体重計を買ったのは失敗だなーと思っている。Bluetooth 体重計のほうがハックしやすいと思うからだ。Wi-Fi 経由で https でサービスと接続されているとサービス側の仕様変更やサービス終了の影響をうけてしまう。そして実際、オムロンはPC側のサービスを終了してしまった。
しかし BT 対応の体重計を買いなおすのも嫌なので、Android アプリが取得しているデータを普通にスクリプト (Perl) で取得できるようにして、Fit にインポートできるようにした。毎日動かせば常に Google Fit 側へデータが同期されるので、たとえ OMRON のサービスが終了しても、最悪データは失われない。リバースエンジニアリングしたので同期スクリプトの公開は控えるが、Google Fit のノウハウだけ記録しておく次第 (BT 体重計から Fit へ同期するアプリケーションなんかを書くときに役立つはずだ)。
Google はウェブの会社で、ユーザーデータの重要性はよくよく理解していると思われるので、サービス終了の際にエクスポートをちゃんと提供することが期待できる。一方でオムロンにそれは期待することはできない。PC版の閲覧サービス終了させてきたしね。