0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GASでfitbitの睡眠データをGoogleCalendarに書き込む

Posted at

はじめに

 fitbitのスマホアプリはなんとなく見づらい。
そのため、GoogleCalendarに睡眠の記録をイベントとして毎日書き込むアプリケーションを作成した。

今回実装したもの

GoogleAppScript(以降GAS)を使用し、毎日のFitbitの睡眠データを、GoogleCalendarに睡眠の記録をイベントとして記載するアプリケーションを作成した。

ソースコード

作成したソースコード

作成したソースコードは以下。

code.js
/*
* シートを作ってくれる
*/
function initApp()
{
 let ss = createSheet('records');
 ss.appendRow([ "TaskName","StartTime", "EndTime", "Duration"]);
 
 ss = createSheet('fitbit-fetched-date');

 ss = createSheet('config');
 ss.getRange(1,1,4,1).setValues([
   ["Email"], 
   ["Fitbit_Event_Color"],
   ["CLIENT_ID"], 
   ["CLIENT_SECRET"],
 ]);
}

function createSheet(sheet_name)
{
 return SpreadsheetApp.getActive().insertSheet(sheet_name);
}

function doEveryday()
{
 let today = new Date();

 if (isFetchedDate(today)) {
   throw new Error(`Fitbit sleep data from ${today.toDateString()} has already been imported.`);
 }

 //refresh
 let service = callCreateService();
 service.refresh();

 //download sleep data
 let res = downloadSleepData(today);
 if (res.getResponseCode() !== 200) {
   throw new Error(res.getContentText());
 } 

 let json = JSON.parse(res);
 let ss = getSheetByName('records');

 if (json.summary.totalSleepRecords === 0) {
   return 0;
 }
 
 for (let i=0;i<json.summary.totalSleepRecords; ++i) {
   let task_name = "睡眠[Fitbit]";
   let start_time = new Date(json.sleep[i].startTime);
   let end_time = new Date(json.sleep[i].endTime);

   //write on records sheet
   ss.insertRowBefore(2);
   let ary = [task_name, start_time, end_time, calcDuration(start_time, end_time)];
   ss.getRange(2,1,1,4).setValues([ary]);

   //write on calendar
   setEventOnCalendar(
     findValueByKey('config','Email'),
     task_name,
     start_time,
     end_time,
     findValueByKey('config',"Fitbit_Event_Color")
   );
 }

 //write on fitbit-fetched-data
 ss = getSheetByName('fitbit-fetched-date');
 ss.appendRow([Utilities.formatDate(today, "JST", "yyyy/MM/dd")]);
}

function isFetchedDate(date)
{
 let ss = getSheetByName('fitbit-fetched-date');
 let list = ss.getRange(1,1, ss.getLastRow(),1).getValues();

 date.setHours(0, 0, 0, 0);
 
 let index = list.findIndex(function(element){return element[0].getTime()=== date.getTime()});
 return !(index === -1);
}

function getSheetByName(sheet_name)
{
 return SpreadsheetApp.getActive().getSheetByName(sheet_name);
}

function callCreateService()
{
 let service_name = 'sample Donwload Sleep Data With Fitbit';
 let authorization_url = 'https://www.fitbit.com/oauth2/authorize';
 let token_url = 'https://api.fitbit.com/oauth2/token';
 let client_id = findValueByKey('config',"CLIENT_ID");
 let client_secret = findValueByKey('config',"CLIENT_SECRET");
 let service = createService(service_name, authorization_url, token_url, client_id, client_secret);
 return service.setTokenHeaders({
   'Authorization': 'Basic ' + Utilities.base64Encode(client_id + ':' + client_secret)
 })
}

function createService(service_name, authorization_url, token_url, client_id, client_secret)
{
 return OAuth2.createService(service_name)
 .setAuthorizationBaseUrl(authorization_url)
 .setTokenUrl(token_url)
 .setClientId(client_id)
 .setClientSecret(client_secret)
 .setPropertyStore(PropertiesService.getUserProperties());
}

function downloadSleepData(date)
{
 let service = callCreateService();

 date = Utilities.formatDate(date, "JST", "yyyy-MM-dd");
 let url = 'https://api.fitbit.com/1.2/user/-/sleep/date/' + date + ".json";

 let options = {
   headers: {Authorization: 'Bearer ' + service.getAccessToken()}
 };

 return UrlFetchApp.fetch(url, options);
}



/**
* configシートに記載した値を取得する
*/
function findValueByKey(sheet_name, key)
{
 let ss = getSheetByName(sheet_name);

 let row = findRow(ss, key, 1);

 return ss.getRange(row, 2).getValue();
}

/**
* 略
*/
function findRow(sheet, needle, column)
{
 let data = sheet.getDataRange().getValues();

 for (let i=0;i<data.length;++i) {
   if (data[i][column-1] === needle) {
     return i+1;
   }
 }
 return 0;
}

/**
* 睡眠時間を計算する
*/
function calcDuration(start_time, end_time)
{
 let duration = (end_time - start_time) / 1000;
 let hour = Math.floor(duration / 3600);
 let minute = Math.floor((duration % 3600) / 60);
 let second = Math.floor(duration % 60);
 return Utilities.formatDate(new Date(0, 0, 0, hour, minute, second), "JST", "HH:mm:ss");
}

/**
* Google Calendarにイベントを追加する
*/
function setEventOnCalendar(email, task_name, start_time, end_time, color)
{
 let cal = CalendarApp.getCalendarById(email);
 cal.createEvent(task_name, start_time, end_time).setColor(color);
}



function doGet(e)
{
 return HtmlService.createHtmlOutput(`<a href='${createAuthorizationUrl()}' target="_blank">認可ページへ</a>`);
}

function createAuthorizationUrl()
{
 let service = callCreateService();
 if (!service.hasAccess()) {
   service = service.setCallbackFunction('authCallback').setExpirationMinutes(60).setScope('sleep');
 }
 return service.getAuthorizationUrl();
}

/**
* 認可後の処理
*/
function authCallback(request)
{
 let service = callCreateService();
 let isAuthorized = service.handleCallback(request);

 if (isAuthorized) {
   ScriptApp.newTrigger('doEveryday').timeBased().atHour(17).everyDays(1).create();
   return HtmlService.createHtmlOutput('Success! You can close this tab.');
 } else {
   return HtmlService.createHtmlOutput('Denied. You can close this tab');
 }
}

外部ライブラリ

外部ライブラリであるOAuth2 for Apps Scriptを使用する。
ライブラリの追加からスクリプトIDを入力して追加ボタンを押下する。

シートの各種説明

①recordsシート
fitbitの睡眠データを格納する

②fitbit-fetched-dateシート
取得した睡眠データの日付を格納する

③configシート
Fitbit APIやGoogle Calendar APIの実行に必要な値を格納する

Fitbit Developerでのアプリケーション申請

fitbit開発者サイトからアプリケーションの申請を行う。
https://dev.fitbit.com/
Screenshot from 2023-11-24 18-36-54.png

・「Application Name」「Description」は各自わかりやすい名前を設定する
・「Application Website URL」「Organization」「Organization Website URL」「Terms of Service URL 」「Privacy Policy URL」は適切な値を設定する
・「OAuth 2.0 Application Type」はPersonalを、「Default Access Type」は Read Onlyを選ぶ

・「Redirect URL」はアプリケーションの動作に影響するため、正確に設定する必要がある。Redirect URLは次を入力する

https://script.google.com/macros/d/${プロジェクトのID}/usercallback

プロジェクトのIDとはGASのID(Apps Scriptプロジェクトの一意の識別子)を指す。
各自のプロジェクトの設定タブから閲覧できる。

申請に成功すると次のような画面が出る。
Screenshot from 2023-11-24 18-42-06 (コピー).png

この画面に表示されている「Client ID」と「Client Secret」は後ほど、Google SpreadSheetのconfigシートに書き込む。

各種設定

事前にinitAppを一度実行しておく。

configシートへの書き込み

Screenshot from 2023-11-28 22-03-09.png

・睡眠データを書き込みたいGoogle CalendarのID(Gmail)をB1に書き込む
・Google Calendar上でのFitbitの睡眠データのイベントの色(0~11の値)をB2に書き込む
・Fitbit Developerでのアプリケーション申請で取得した「Client ID」と「Client Secret」をそれぞれB3、B4に書き込む

実行

ウェブアプリケーションとして公開し、アクセスを一度するだけで終了。

Web アプリケーションのデプロイ

ウェブアプリケーションとしてデプロイする。
Screenshot from 2023-11-22 21-57-33.png

このように表示されたURLにアクセスする。

アクセスすると、以下のような画面が開くので、リンクを踏む。
Screenshot from 2023-11-24 18-46-55.png

fitbitログイン画面がでるので、各自のfitbitアカウントでログインすると

Screenshot from 2023-11-24 18-47-36.png
のようにfitbitの認可ページがでる。ここでは睡眠状態を選択し、許可を押す。

最終的に
Screenshot from 2023-11-24 18-47-55.png

のような、画面がでれば成功。以降は毎日寝るだけ。

トリガーの設定

今回はソースコードの以下の部分

code.js
ScriptApp.newTrigger('doEveryday').timeBased().atHour(17).everyDays(1).create();

で17頃時頃にトリガーを作成した。
が、17時である理由は特になく(17時までに起床しているだろうという前提のみ)、atHour関数の引数は好きなように設定できる。

おわりに

 fitbitデバイスを装着して寝ることを忘れない。
また、トリガーで設定した時刻までに同期をしておく。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?