要点
- タニタ体組成計RD-907で毎日体組成を測定しているが、fitbitやGoogleFitにもデータを同期したい
- Google Apps Scriptを毎日定期実行し、タニタ体組成計のデータを取得できるHealthPlanet APIとfitbit Web-APIを利用してHealthPlanet→fitbitのデータ転送を実現
- タニタの体組成計のデータはスプレッドシートに保存
- fitbit→GoogleFitはAndroidアプリのFitToFitを利用
使用ツール
ツールの用意
1. HealthPlanet API利用の準備
HealthPlanet APIを利用するにあたって、HealthPlanetにてClient IDとClient secretを取得する必要がある。
こちらのページを参考にさせていただいた。
HealthPlanet APIのページにて下図のようにClient IDとClient secretを取得できれば完了。下図では該当部は白抜きしている。
2. Fitbit API利用の準備
HealthPlanetと同様にFitbitのAPI利用登録を行う。
こちらのページを参考にさせていただいた。
Fitbit devにてAPIの利用登録ができれば完了。
3. Google Apps ScriptへのOAuthライブラリの登録
API利用のためのOAuth周りについては全くの無知なのでライブラリを利用する。
こちらのOauth2ライブラリを利用した。
Google Apps Scriptにライブラリを登録するにはこちらのコードをライブラリに追加すれば良い。
1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
4. スプレッドシートの用意
Google Apps Scriptと連携したスプレッドシートはこのようなフォーマットにした。
実装
1. OAuthに必要な情報
//HealthPlanet用
var HP_CLIENT_ID = '*****'; //Health Planet の Client ID
var HP_CLIENT_SECRET = '*****'; //Health Planet の Client Secret
var HP_OAUTH_URL = 'https://www.healthplanet.jp/oauth/auth';
var HP_TOKEN_URL = 'https://www.healthplanet.jp/oauth/token';
var HP_CODE = '*****'; //リダイレクトで返ってきたCODEを手動でコピペ
var HP_ACCESS_TOKEN = '*****'; //取得したアクセストークンを手動でコピペ
var HP_REFRESH_TOKEN = '*****'; //使用しない?
//Fitbit用
var FB_CLIENT_ID = '*****'; //Fitbit の Client ID
var FB_CLIENT_SECRET = '*****'; //FitBit の Client Secret
var FB_OAUTH_URL = 'https://www.fitbit.com/oauth2/authorize';
var FB_TOKEN_URL = 'https://api.fitbit.com/oauth2/token';
var FB_USERID = '*****'; //Fitbit devに記載されている
2. HealthPlanetからデータを取得する
今回同期したいのは体重と体脂肪率だけだが、一通りデータを取得してスプレッドシートに追記する。既にスプレッドシートにデータが有るかどうかは日付で判定する。HealthPlanetから直近の2ヶ月分のデータを取得する。OAuth処理が終わったあとはhpGetData()を定期実行する。
OAuth周りのことがよくわからないので、HealthPlanetの場合はライブラリがうまく機能せず(具体的にはCallback関数が呼ばれない、リフレッシュトークンを利用したアクセストークン更新方法がわからない)、アクセスコードやアクセストークンを手動でフェッチして「1. OAuthに必要な情報」に転記している。
良い方法があったら教えていただきたい。
// 本来であればリフレッシュトークンを利用したアクセストークンの更新によってhasAccessがtrueになるはずだが、HealthPlanetの仕様のために常にfalse
function hpRun() {
var service = hpGetService();
// アクセストークンが有効か調べる
if (service.hasAccess()) {
// 本当はここに実行コードを書きたいが、hasAccessがtrueにならない
Logger.log('hp hasAccess OK')
}else{
var authorizationUrl = service.getAuthorizationUrl();
Logger.log(authorizationUrl);
}
}
// Callbackで戻ってきたときに呼ばれる→HealthPlanetでは呼ばれない
function hpAuthCallback(request) {
logger.Log('hpAuthCallback')
var service = hpGetService();
var isAuthorized = service.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success!');
} else {
return HtmlService.createHtmlOutput('Denied.');
}
}
// Authorizationをリセット
function hpClearService(){
hpGetService().reset();
}
// OAuth2の設定
function hpGetService(){
return OAuth2.createService('HealthPlanet') // 任意の名前
.setAuthorizationBaseUrl(HP_OAUTH_URL)
.setTokenUrl(HP_TOKEN_URL)
.setClientId(HP_CLIENT_ID)
.setClientSecret(HP_CLIENT_SECRET)
.setCallbackFunction('hpAuthCallback')
.setPropertyStore(PropertiesService.getUserProperties())
.setScope('innerscan')
}
/*
CODEを元にアクセストークンを取得する
*/
function hpGetToken(){
url = HP_TOKEN_URL;
var payload = {
'client_id' : HP_CLIENT_ID,
'client_secret' : HP_CLIENT_SECRET,
'redirect_uri' : 'https://www.healthplanet.jp/success.html',
'code' : HP_CODE,
'grant_type' : 'authorization_code'
};
var options = {
'method' : 'POST',
'payload': payload,
};
// トークンの取得
var response = UrlFetchApp.fetch(url, options);
var data = JSON.parse(response)
Logger.log(data)
}
/*
HealthPlanetから直近の2ヶ月分のデータを取得する
*/
function hpGetData(){
// スプレッドシートを取得する
var hpSS = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("HealthPlanet");
var hpSS_Date = hpSS.getRange(2,1,hpSS.getLastRow(),1).getValues();
// スプレッドシート上のデータの日付をフォーマットしてリスト化
hpDateList = []
for(var i = 0; i < hpSS_Date.length; i++){
var ssDateFormatted = new Date(hpSS_Date[i]);
hpDateList.push(ssDateFormatted.getYear() + '/' + ('0' + String(Number(ssDateFormatted.getMonth()) + 1)).slice(-2) + '/' + ('0' + ssDateFormatted.getDate()).slice(-2));
}
url = 'https://www.healthplanet.jp/status/innerscan.json';
// 2ヶ月前の日付を取得
fromDate = new Date()
fromDate.setMonth(fromDate.getMonth() - 2)
fromDate = Utilities.formatDate(fromDate, 'Asia/Tokyo', 'yyyyMMdd') + '000000';
// payloadに必要な情報
/*
date (必須)
from to の日付タイプを指定する。
0 : 登録日付
1 : 測定日付
from
取得期間 from を指定する。yyyyMMddHHmmss 形式で指定し、必ず from < to でなければならない
未指定の場合は3ヶ月前が指定される
to
取得期間 to を指定する。yyyyMMddHHmmss 形式で指定し、必ず from < to でなければならない
未指定の場合は現時刻が指定される
tag
取得する測定部位を指定する。未指定の場合はデフォルトの値を取得する。カンマ区切りで指定する
6021 : 体重 (kg)
6022 : 体脂肪率 (%)
6023 : 筋肉量 (kg)
6024 : 筋肉スコア
6025 : 内臓脂肪レベル2(小数点有り、手入力含まず)
6026 : 内臓脂肪レベル(小数点無し、手入力含む) → 使用されていない
6027 : 基礎代謝量 (kcal)
6028 : 体内年齢 (才)
6029 : 推定骨量 (kg)
*/
var payload = {
'access_token' : HP_ACCESS_TOKEN,
'date' : '1',
'tag' : '6021,6022,6023,6024,6025,6027,6028,6029',//6026を除く
'from' : fromDate
};
var options = {
'method' : 'POST',
'payload': payload,
};
// データを取得する
var response = UrlFetchApp.fetch(url, options);
var data = JSON.parse(response); //このdataは日付に対して6021~6029のうちのデータを1つもつため、1日分のデータが分かれている
oneday_datalist = []; // 1日分のデータを保持するリスト
prev_date = ''; // 前日の日付
// 取得したデータのうち、スプレッドシートの日付リストhpDateListに日付が存在しないものをスプレッドシートに追記
for(var i = 0; i < data['data'].length; i++){
date = data['data'][i].date;
dateTimeFormatted = date.substr(0,4) + '/' + date.substr(4,2) + '/' + date.substr(6,2) + ' ' + date.substr(8,2) + ':' + date.substr(10,2)
dateFormatted = date.substr(0,4) + '/' + date.substr(4,2) + '/' + date.substr(6,2)
// 日付で既存有無の判断
if(hpDateList.indexOf(dateFormatted)==-1.0){
if(prev_date == ''){ // for文の初回のみ呼ばれる
oneday_datalist.push(dateTimeFormatted);
}else if(date != prev_date){ // dataの日付が変わった場合にoneday_datalistにまとめたデータをスプレッドシートに追記する
hpSS.appendRow(oneday_datalist);
oneday_datalist = [];
oneday_datalist.push(dateTimeFormatted);
}
if(oneday_datalist.length < 9){ // 同じ日時に複数回測定していると9行目以降に追記されるが、そのデータは不要なので無視する
oneday_datalist.push(data['data'][i].keydata);
};
prev_date = date;
};
};
// 日付で昇順に並び替え
ssHP.getRange(2,1,ssHP.getLastRow() - 1,ssHP.getLastColumn()).sort({column:1, ascending:true})
}
3. Fitbitにデータを転送する
Fitbitから過去1年分の体重データを取得し、Fitbitに体重データが無いものをHealthPlanetから転送する。判定は日付で実行している。fbRun()を定期実行する。
function fbRun() {
var service = fbGetService();
// アクセストークンを持ってるか調べる
if (service.hasAccess()) {
Logger.log('fbRun already authorized! Start processing!');
// 今日から1年間遡ってfitbitの体重データをリストで取得する
fbWeightList = fbGetWeightList(service);
/*
HealthPlanetのデータのうち、fitbitのデータに存在しないものを日付を元に探す
*/
// HealthPlanetのスプレッドシートからデータを取得する
var hpSS = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("HealthPlanet");
var hpData = hpSS.getRange(2,1,hpSS.getLastRow() - 1,hpSS.getLastColumn()).getValues();
// fitbitにデータの存在する日付をリストで保有する
fbDateList = [];
for(var i = 0; i < fbWeightList.length; i++){
fbDateList.push(fbWeightList[i]['date']);
}
// HealthPlanetのスプレッドシートデータの日付がfitbitデータの日付にあるかどうかを判定する
for(var i = 0; i < hpData.length; i++){
var hpDate = new Date(hpData[i][0]);
hpDateFormatted = hpDate.getYear() + '-' + ('0' + String(Number(hpDate.getMonth()) + 1)).slice(-2) + '-' + ('0' + hpDate.getDate()).slice(-2);
hpTimeFormatted = ('0' + hpDate.getHours()).slice(-2) + ':' + ('0' + hpDate.getMinutes()).slice(-2) + ':00';
// fitbitに存在しない場合
if(fbDateList.indexOf(hpDateFormatted)==-1.0){
fbPostWeight(service, hpDateFormatted, hpTimeFormatted, hpData[i][1], hpData[i][2]); // 日付、時刻、体重、体脂肪率を引き数で渡す
hpSS.getRange(i + 2, 10).setValue(new Date()); // fitbitに同期した日付をスプレッドシートに追記する
}
}
}else{
var authorizationUrl = service.getAuthorizationUrl();
Logger.log(authorizationUrl);
}
}
//Callbackで戻ってきたときに呼ばれる
function fbAuthCallback(request) {
var service = fbGetService();
var isAuthorized = service.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success!');
} else {
return HtmlService.createHtmlOutput('Denied.');
}
}
// リセット
function fbClearService(){
fbGetService().reset();
}
// OAuth2の設定
function fbGetService(){
return OAuth2.createService('Fitbit') // 任意の名前
.setAuthorizationBaseUrl(FB_OAUTH_URL)
.setTokenUrl(FB_TOKEN_URL)
.setClientId(FB_CLIENT_ID)
.setClientSecret(FB_CLIENT_SECRET)
.setCallbackFunction('fbAuthCallback')
.setPropertyStore(PropertiesService.getUserProperties())
.setScope('weight')
.setTokenHeaders({'Authorization': 'Basic ' + Utilities.base64Encode(FB_CLIENT_ID + ':' + FB_CLIENT_SECRET)});
}
function logRedirectUri() {
Logger.log(OAuth2.getRedirectUri());
}
// 今日の日付から1年分遡って体重データを取得する
function fbGetWeightList(service){
dateList = [];
weightList = [];
// 1年分の日付を1ヶ月毎に分けてリスト化
for(var i = 0; i < 12; i++){
fetchDate = new Date();
fetchDate.setMonth(fetchDate.getMonth() - i)
fetchDate = Utilities.formatDate(fetchDate, 'Asia/Tokyo', 'yyyy-MM-dd');
dateList.push(fetchDate);
}
// 1ヶ月分ごとにデータを取得
for(var i = 0; i < dateList.length; i++){
url = 'https://api.fitbit.com/1/user/' + FB_USERID + '/body/log/weight/date/' + dateList[i] + '/1m.json';
var headers = {
'Authorization': 'Bearer ' + service.getAccessToken()
};
var options = {
'method' : 'GET',
'headers' : headers,
'muteHttpExceptions': true
};
// データ取得
var response = UrlFetchApp.fetch(url, options);
var data = JSON.parse(response);
Array.prototype.push.apply(weightList, data['weight'])
}
return weightList
}
//渡された日時の体重と体脂肪率をfitbitに同期する
function fbPostWeight(service, date, time, weight, fat){
urlWeight = 'https://api.fitbit.com/1/user/' + FB_USERID + '/body/log/weight.json';
urlFat = 'https://api.fitbit.com/1/user/' + FB_USERID + '/body/log/fat.json';
// アクセストークンをheadersで渡す
var headers = {
'Authorization': 'Bearer ' + service.getAccessToken()
};
var payloadWeight = {
'weight' : weight,
'date' : date,
'time' : time
};
var payloadFat = {
'fat' : fat,
'date' : date,
'time' : time
};
var optionsWeight = {
'method' : 'POST',
'payload': payloadWeight,
'headers' : headers,
'muteHttpExceptions': true
};
var optionsFat = {
'method' : 'POST',
'payload': payloadFat,
'headers' : headers,
'muteHttpExceptions': true
};
// データ投稿
var responseWeight = UrlFetchApp.fetch(urlWeight, optionsWeight);
Logger.log(JSON.parse(responseWeight))
var responseFat = UrlFetchApp.fetch(urlFat, optionsFat);
Logger.log(JSON.parse(responseFat))
}
4. Google Apps Scriptを定期実行する
hpGetData()とfbRun()を1日に1度定期実行する。スプレッドシートのデータ数が増えすぎても困るので、あとは1年より前のデータは自動削除するスクリプトを作れば完璧。