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

## はじめに
　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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3586658/30a40576-4ecb-dff8-6e6d-1b308ebaa542.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3586658/99c3a072-5ce1-c920-788d-b08125e3d914.png)

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

## 各種設定
事前にinitAppを一度実行しておく。
### configシートへの書き込み
![Screenshot from 2023-11-28 22-03-09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3586658/eb0372f6-057b-dcc0-4fe6-0d8d500275f1.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3586658/e17752b9-c02e-c96f-4727-13f26784b15b.png)

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

アクセスすると、以下のような画面が開くので、リンクを踏む。
![Screenshot from 2023-11-24 18-46-55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3586658/457142d6-2e6b-9e37-1ee1-bbdcdd319694.png)

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


![Screenshot from 2023-11-24 18-47-36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3586658/ff162614-f21e-7a01-cfb7-5f20f6c68c60.png)
のようにfitbitの認可ページがでる。ここでは睡眠状態を選択し、許可を押す。

最終的に
![Screenshot from 2023-11-24 18-47-55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3586658/8f2f150e-5682-9530-a534-02dee4a2cc46.png)


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

## トリガーの設定
今回はソースコードの以下の部分
```code.js
ScriptApp.newTrigger('doEveryday').timeBased().atHour(17).everyDays(1).create();
```
で17頃時頃にトリガーを作成した。
が、17時である理由は特になく(17時までに起床しているだろうという前提のみ)、atHour関数の引数は好きなように設定できる。

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