#要点
- タニタ体組成計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年より前のデータは自動削除するスクリプトを作れば完璧。