【GAS】FitbitのWebAPIで歩数データを取り出してみよう!

技術

ウォーキングラリーに参加しました

今月は会社のイベントとしてウォーキングラリーが開催されました。
1か月間毎日、歩数を記録して毎週月曜日に前週の月曜~日曜までの1週間の合計歩数を会社のシステムに登録する、というものです。

5月に予定されている健康診断に向けて、健康増進を目的に開催されています。

参加は任意ですが、今回私は初めて参加してみました。

私の場合はPixel Watchを身につけて歩数をFitbitに記録して、アプリで確認できるようにしています。

なので、月曜日の朝に先週の一週間分の歩数を手で合計してシステムに登録するって感じなんですが、月曜日の朝は忙しいので、正直ゆっくり計算してる余裕なんてない。

というわけで、タイトルにある通り

GASでFitbit歩数取得計算をスクリプト化して自動化してしまおう!!

というのが今回の記事です。

Access Tokenを作る

FitbitのWebAPIを利用するためには、最初にDevelper用のページでAccess Tokenを作る必要があります。

この辺りの手順はいろいろなページで既に紹介されています。

なので、「いまさら」って感じはありますが一応2023年4月時点の手順ということで、このページでも紹介しておこうと思います。

アプリケーション登録

まずはDeveloper用のページにアクセスします。
ログインを求められたら、Fitbitにアクセスする時のいつものメールアドレスとパスワードでログインできます。

Login
description

ログインできたらREGISTER ANN APPというタブを選んで、必要な項目を入力します。
ポイントはこんな感じ(↓)です。

  • Application Name と Description は何でもいいです。
    あと、Organization も何でも構いません。
  • URL関係は全て「https://localhost」でかまいません。
    たとえば Redirect URL という設定は本来はOAuthの認証後にリダイレクトするためのURLを指定しなければいけませんが、今回はサービス化を考えないで、ひとまずAccess tokenをただ作りたいだけなのでlocalhostにリダイレクトされて問題ありません。
  • OAuth 2.0 Application Type は「Server」
  • Default Access Type は「Read Only」

※ Descriptionは10文字以上必要らしいです。

必要項目を入力してアプリケーション登録が完了するとこんな感じの画面になります。

OAuth 2.0 Client IDClient Secreteの値がこの後必要になります。

Access Token作成

先程のアプリケーション登録後のページ下部にあるOAuth 2.0 Tutorialというリンクか、下記のURLにアクセスします。

Fitbit Development: OAuth 2.0 Tutorial
You'll fit in here. Using JavaScript, CSS, and SVG, developers now have a fast, easy way to build apps and clock faces f...

このページはAccess Tokenの作成を手助けしてくれます。
このページに、先程確認したClient IDとClient Secretを入力します。
また、Application Typeには「Server」を選択します。

  1. 少し下にスクロールするとPKCE Code VerifierPKCE Code Challenge、さらにStateを「GENERATE」ボタンで生成します。
  2. Scopesは「activity」にチェックを付けます。
    他はチェックをしてもしなくてもかまいません。

ここまで設定するとURLが表示されるのでクリックしてOAuthの認証画面にアクセスします。

するとFitbitのログイン画面の後でアプリの認証を求められるので、「すべて許可する」にチェックを付けて「許可」ボタンをクリックします。

ここでlocalhostにリダイレクトされると思います。
Redirect URLに「https://localhost」を指定していたためです。

localhostにWebサーバーを立てていなければエラー画面になっていると思いますが、必要なのはブラウザのアドレスバーに表示されるURLです。
このURLをコピーして、OAuth 2.0 Tutorialのページに戻ります。

OAuth 2.0 TutorialのページでHandle the Redirectに先ほどコピーしたURLを張り付けると、curlコマンドが自動で生成されます。
このcurlコマンドをコピーしてコマンドプロンプトからリクエストしてもかまいませんが、ページ内の「SUBMIT REQUEST」ボタンからリクエストすることもできます。

これでAccess Tokenが生成できました。
あわせてReresh Tokenもできていて、こちらも使います。

ちなみに、Fitbitの認証画面で認証を「許可」してから、curlコマンドの実行まで時間が空いてしまうと、Access Tokenが取得できず認証エラーになってしまいますので、ご注意ください。

もし、認証エラーになってしまったら、もう一度Fitbitの認証画面で「すべて許可する」ところからやり直してください。

歩数取得をGASスクリプトで実装する

Access Tokenが準備できたらいよいよScriptを実装します。
まずはFitbitのAPIを処理する部分からです。

class FitbitApi {
  
  constructor (client_id, client_secret, access_token, refresh_token) {
    this.client_id = client_id;
    this.client_secret = client_secret;
    this.access_token = access_token;
    this.refresh_token = refresh_token;
  }

  getSteps(begin, end) {
    var url = `https://api.fitbit.com/1/user/-/activities/steps/date/${begin}/${end}.json`;
    var options = {
      'method': 'get',
      'headers': {
        'Authorization': `Bearer ${this.access_token}`
      },
      'muteHttpExceptions': true
    };

    let res = UrlFetchApp.fetch(url, options);
    if (res.getResponseCode() == 200) {
      let content = JSON.parse(res.getContentText());
      return content['activities-steps'];
    } else if (res.getResponseCode() == 401) {
      throw FitbitApi.UnauthorizedError;
    } else {
      console.error(res.getContentText());
    }
  }

  tokenRefresh() {
    let url = `https://api.fitbit.com/oauth2/token?grant_type=refresh_token&client_id=${this.client_id}&refresh_token=${this.refresh_token}`;
    let options = {
      'method': 'post',
      'contentType': 'application/x-www-form-urlencoded',
      'headers': {
        'Authorization': `Basic ${Utilities.base64Encode(this.client_id + ":" + this.client_secret, Utilities.Charset.UTF_8)}`
      },
      'muteHttpExceptions': true
    };
    let res = UrlFetchApp.fetch(url, options);
    if (res.getResponseCode() == 200) {
      let content = JSON.parse(res.getContentText());
      this.access_token = content.access_token;
      this.refresh_token = content.refresh_token;
    } else {
      console.error(res.getContentText());
    }
  }
}
FitbitApi.UnauthorizedError = Error("Access token was expired. Please take new access token by use refresh token.");

まずFitbitApiというクラスを作りました。

FitbitApiクラスはコンストラクタとgetStepstokenRefreshという2つのメソッドを持つクラスです。

  • constructor (client_id, client_secret, access_token, refresh_token)
  • getSteps (begin, end)
  • tokenRefresh ()

コンストラクタでFitbitのWebAPIにアクセスするためのAccess TokenとRefresh Tokenを指定します。
また、Access Tokenを生成する過程で参照した OAuth 2.0 Client ID と Client Secret も トークンリフレッシュ時に必要となります。

getSteps (begin, end)

getStepsはその名の通り歩数を取得するメソッドです。
コンストラクタを取得した Access Token を使って Fitbit の WebAPI を呼び出して歩数を取得しています。
引数の「begin」と「end」は “yyyy-MM-dd” 形式の日付文字列で歩数を取得する期間を指定するためのものです。

歩数を取得するには例えば2023/4/1~2023/4/30の歩数を取得したい場合

https://api.fitbit.com/1/user/-/activities/steps/date/2023-04-01/2023-04-30.json

というURLに対してGETメソッドでリクエストを投げます。
この時リクエストヘッダの「Authorization」に “Bearer ${this.access_token}” を指定することで認証させています。

ただし、Access Tokenには有効期限があっていつまででも使えるわけではありません。
有効期限が切れて認証エラーとなるとAPIはステータスコード:401(Unauthored)を返してきます。

そこで、401を受け取った時は「UnauthorizedError」という独自に用意しておいた例外をthrowして、他のエラーと区別できるようにしました。

こうすると401で認証エラーとなった時、トークンリフレッシュして新しいAccess Tokenを取得する仕組みを組み込むことができます。

tokenRefresh ()

tokenRefresh は Access Token の有効期限が切れて認証エラーとなった時、新しい Access Token と Refresh Token を取得するためのものです。

トークンリフレッシュにはコンストラクタで指定した client_id、client_secret、refresh_tokenを使います。

https://api.fitbit.com/oauth2/token?grant_type=refresh_token&client_id=${this.client_id}&refresh_token=${this.refresh_token}

というURLに対してPOSTメソッドでリクエストを投げます。
そして、この時リクエストヘッダの「Authorization」には 「client_id + “:” + client_secret」というテキストをBase64エンコードした【認証コード】を “Basic ${認証コード}” と指定します。

ステータスコード:200 で帰ってきたら成功なので access_token と refresh_token をレスポンスボディから取得して、次回以降のリクエストに利用できます。

FitbitApiクラスの利用部分の実装

const CLIENT_ID = "23QQTK";
const CLIENT_SECRET = "6cdbea143845e2e5330004ffb918de45";

function myFunction() {
  // 直近1週間の日付を取得
  let begin = new Date(), end = new Date();
  begin.setDate(begin.getDate() - 7);
  end.setDate(end.getDate() - 1);
  let beginStr = Utilities.formatDate(begin, "JST", "yyyy-MM-dd");
  let endStr = Utilities.formatDate(end, "JST", "yyyy-MM-dd");

  // FitBit APIを使って歩数を取得
  let fitbit = new FitbitApi(
    CLIENT_ID,
    CLIENT_SECRET,
    ScriptProperties.getProperty('FITBIT_ACCESS_TOKEN'),
    ScriptProperties.getProperty('FITBIT_REFRESH_TOKEN'));
  try
  {
    var steps = fitbit.getSteps(beginStr, endStr);
  } catch (ex) {
    if (ex == FitbitApi.UnauthorizedError) {
      // トークンが期限切れたらリフレッシュした上でリトライ
      fitbit.tokenRefresh();
      ScriptProperties.setProperty('FITBIT_ACCESS_TOKEN', fitbit.access_token);
      ScriptProperties.setProperty('FITBIT_REFRESH_TOKEN', fitbit.refresh_token);
      var steps =  fitbit.getSteps(beginStr, endStr);
    } else {
      console.error(ex);
    }
  }

  console.log(steps);
}

FitbitApiクラスを利用する部分の実装はこんな感じになりました。

Access TokenとRefresh Tokenはスクリプトプロパティで管理するようにしました。

まず、FitbitApiクラスを new してインスタンスを生成しています。
ここでスクリプトプロパティから取得したAccess TokenとRefresh Tokenを指定します。
Client IDとClient Secretは固定でかまわないので定数定義にしました。

最初に fitbit.getSteps を呼び出して歩数を取得します。
何事もなく成功すれば歩数が取得できているはずです。

失敗すると例外がthrowされるので try – catch を使って、エラーを捕まえます。
この時、例外がFitbitApi.UnauthorizedErrorであれば認証エラーなので fitbit.tokenRefresh した上で再度 fitbit.getSteps で歩数取得をリトライする仕組みにしました。

ちなみにtokenRefreshが成功したら、新しくなったAccess Token、Refresh Tokenをスクリプトプロパティに保存することで、次回以降の実行に利用できるようになっています。

まとめ

最後にScriptの全体を掲載しておきます。

const CLIENT_ID = "23QQTK";
const CLIENT_SECRET = "6cdbea143845e2e5330004ffb918de45";

function myFunction() {
  // 直近1週間の日付を取得
  let begin = new Date(), end = new Date();
  begin.setDate(begin.getDate() - 7);
  end.setDate(end.getDate() - 1);
  let beginStr = Utilities.formatDate(begin, "JST", "yyyy-MM-dd");
  let endStr = Utilities.formatDate(end, "JST", "yyyy-MM-dd");

  // FitBit APIを使って歩数を取得
  let fitbit = new FitbitApi(
    CLIENT_ID,
    CLIENT_SECRET,
    ScriptProperties.getProperty('FITBIT_ACCESS_TOKEN'),
    ScriptProperties.getProperty('FITBIT_REFRESH_TOKEN'));
  try
  {
    var steps = fitbit.getSteps(beginStr, endStr);
  } catch (ex) {
    if (ex == FitbitApi.UnauthorizedError) {
      // トークンが期限切れたらリフレッシュした上でリトライ
      fitbit.tokenRefresh();
      ScriptProperties.setProperty('FITBIT_ACCESS_TOKEN', fitbit.access_token);
      ScriptProperties.setProperty('FITBIT_REFRESH_TOKEN', fitbit.refresh_token);
      var steps =  fitbit.getSteps(beginStr, endStr);
    } else {
      console.error(ex);
    }
  }

  console.log(steps);
}

class FitbitApi {
  
  constructor (client_id, client_secret, access_token, refresh_token) {
    this.client_id = client_id;
    this.client_secret = client_secret;
    this.access_token = access_token;
    this.refresh_token = refresh_token;
  }

  getSteps(begin, end) {
    var url = `https://api.fitbit.com/1/user/-/activities/steps/date/${begin}/${end}.json`;
    var options = {
      'method': 'get',
      'headers': {
        'Authorization': `Bearer ${this.access_token}`
      },
      'muteHttpExceptions': true
    };

    let res = UrlFetchApp.fetch(url, options);
    if (res.getResponseCode() == 200) {
      let content = JSON.parse(res.getContentText());
      return content['activities-steps'];
    } else if (res.getResponseCode() == 401) {
      throw FitbitApi.UnauthorizedError;
    } else {
      console.error(res.getContentText());
    }
  }

  tokenRefresh() {
    let url = `https://api.fitbit.com/oauth2/token?grant_type=refresh_token&client_id=${this.client_id}&refresh_token=${this.refresh_token}`;
    let options = {
      'method': 'post',
      'contentType': 'application/x-www-form-urlencoded',
      'headers': {
        'Authorization': `Basic ${Utilities.base64Encode(this.client_id + ":" + this.client_secret, Utilities.Charset.UTF_8)}`
      },
      'muteHttpExceptions': true
    };
    let res = UrlFetchApp.fetch(url, options);
    if (res.getResponseCode() == 200) {
      let content = JSON.parse(res.getContentText());
      this.access_token = content.access_token;
      this.refresh_token = content.refresh_token;
    } else {
      console.error(res.getContentText());
    }
  }
}
FitbitApi.UnauthorizedError = Error("Access token was expired. Please take new access token by use refresh token.");

見ていただいてわかる通り、今回のスクリプトでは取得した歩数を console.log しているだけですが、実際に運用しているスクリプトでは計算して会社のシステムに登録する処理まで実装しています。

なので、毎週月曜日の朝、何もしなくても勝手に前週の歩数の合計がシステムに登録される仕組みになっています。

この辺りの実装を紹介してもあまり意味はないので、今回は割愛しました。
代わりにTwitterなんかに投稿する仕組みにしてもいいかもしれませんね。

コメント

タイトルとURLをコピーしました