CloudFunctionsでFitbitアクティビティデータを取得する

## 概要
- CloudFunctionsを使って、Fitbitのアクティビティデータを取得したい。
- そのやり方をまとめました。

## 環境
firebase-tools 11.16.0

## 前提条件
- [Node.js](https://nodejs.org/ja/)のダウンロード・インストール済み

## Fitbitのアクティビティデータって何？
- Fitbitのスマートウォッチ上で、**エクササイズ > XXの開始** を行って記録する、運動時のデータです。
    - エクササイズの種類には、 ウォーク、ラン、バイク、スイム、ワークアウト、トレッド　があります。
    - 心拍、緯度経度、時刻などが記録されます。
- [Fitbit Home](https://www.fitbit.com/) > サインイン > ダッシュボード > 記録 > 運動 > アクティビティ履歴 > 詳細を表示 > で表示されるデータがアクティビティデータです。
    - URLの末尾番号がアクティビティの`log_id`です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1834744/9e7f5e54-9db2-1977-cb6a-f39fd219cbac.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1834744/2aed9af4-346b-3b3f-b0ab-b39967864196.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1834744/36919061-cd45-40b0-ac1a-4027ccc92016.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1834744/e573a4f3-5cc3-faaa-7838-930194cc1ee0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1834744/d6c87f8f-6f51-a2bd-d907-9dc7f971dac5.png)

## アクティビティデータの取得の流れ
1. `log_id_list`を取得する
    - ex) {510001, 510005, 510007, 510009}
1. `log_id`を指定して.tcxデータを取得する



## ライブラリインストール
- CloudFunctionsフォルダ直下へ以下のライブラリをインストールします。
  
- Axios
    - GET, POST用
    - 公式ドキュメント https://www.npmjs.com/package/axios
```terminal:my-project/functions
npm install axios
```

## ログIDリストを取得するサンプルコード
- [Get Activity Log Listの公式ドキュメント](https://dev.fitbit.com/build/reference/web-api/activity/get-activity-log-list/) にアクティビティの`log_id_list`を取得するCURLコマンドが載っています。
- 公式ドキュメント > Query Parameters に記載ある通り、`beforeDate`か`afterDate`のどちらかを指定が必要。今回は`afterDate`を指定したサンプルです。
    - 実際に使う場合は、本日の日付-n日を指定すると良いと思います。
- `status===401`の場合はアクセストークンの有効期限切れなので、リフレッシュトークンを使用してアクセストークンの更新を行うことが必要です。
    - やり方は[以前書いた記事](https://qiita.com/zono345/items/43139a9b0b73a46c0d30) を参照。
- [Curl Converter](https://curlconverter.com/) のサイトを使うと、CURLコマンドをnode-fetchやaxiosの書き方へコンバートできるので便利です。


```javascript:my-project/functions/index.js
const functions = require("firebase-functions");
const axios = require('axios')

// n日前のDate型データを取得する
function getPastDate(n) {
    let pastDate = new Date()
    pastDate.setDate(pastDate.getDate() - Number(n))
    return pastDate
};

// Date型データをFitbitのGETメソッドに適したyyyy-MM-ddに整形するメソッド (例: "2022-09-01")
function formatDateForFitbit(mDate) {
    const year = mDate.getFullYear()
    const month = ('00' + (mDate.getMonth()+1)).slice(-2)
    const day = ('00' + mDate.getDate()).slice(-2)
    const convertedDate = year+"-"+month+"-"+day
    return convertedDate
};

// ログIDリストの取得
async function getLogIdList(accessToken) {
    // アクセストークン
    const currentAccessToken = accessToken

    // 何日前からのlog_idを取得するか(今回は直近14日間のデータとする)
    const pastDay = 14
    // log_idを取得する開始日付(yyyy-MM-ddの形式で任意の日付を指定する。例:"2022-10-20")
    const afterDate = formatDateForFitbit(getPastDate(pastDay))

    // 取得するデータ最大数(最大100)
    const dataCountLimit = "100"

    // log_idを格納する配列
    let logIdArray = new array()

    // Fitbitからactivity_log_id_listを取得
    await axios
        .get(
            'https://api.fitbit.com/1/user/-/activities/list.json', {
                params: {
                    'afterDate': afterDate,
                    'sort': 'asc',
                    'offset': '0',
                    'limit': dataCountLimit
                },
                headers: {
                    'accept': 'application/json',
                    'authorization': 'Bearer '+currentAccessToken,
                }
        })
        .then(response => {
            // 取得したログを配列へ格納
            for (let i = 0; i < response.data.activities.length; i++) {
                // 取得したlogIdは数値型なので扱いやすいString型に変換する
                const logId = String(response.data.activities[i].logId)
                logIdArray.push(logId)
            }
        })
        .catch(error => {
            console.log("log_id_listを取得に失敗 -> error ", error)
            console.log("status: "+error.response.status)
            if(error.response.status===401) {
                // アクセストークンが有効期限切れの場合(status401)
                // アクセストークンを更新する ※ここは割愛します
                // ...
                // ...
                // ...
            }
        })
}
```


## アクティビティデータを取得するサンプルコード
- [Get Activity TCXの公式ドキュメント](https://dev.fitbit.com/build/reference/web-api/activity/get-activity-tcx/) に指定した`log_id`のアクティビティデータを取得するCURLコマンドが載っています。

```javascript:my-project/functions/index.js
const functions = require("firebase-functions");
const axios = require('axios')

// ログIDリストの取得
async function getActivityData(mAccessToken, mLogId) {
    const currentAccessToken = mAccessToken
    const logId = mLogId

    await axios
        .get(
            "https://api.fitbit.com/1/user/-/activities/"+logId+".tcx", {
                headers: {
                    'accept': 'application/json',
                    'authorization': 'Bearer '+currentAccessToken,
                    'content-type': 'application/json',
                }
            })
        .then(async(response) => {
            // 受信データはXML形式になっています
            const xmlDoc = response.data

            // xmlDocから必要なデータを取得・加工 ※ここは割愛します
            // ...
            // ...
            // ...


        })
        .catch(error => {
            console.log("activity_dataの取得失敗 -> error: ", error)
        })
}
```

- ちなみに、`const xmlDoc = response.data`で受け取るデータサンプルはこんな感じです。
- アクティビティデータ記録時に、スマホとBLE接続していない場合は、GPS情報を取得できないので、`LatitudeDegrees`や`LongitudeDegrees`のデータは入っていないです。

```xml:response.data
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TrainingCenterDatabase xmlns="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2">
    <Activities>
        <Activity Sport="Other">
            <Id>2022-09-11T10:39:59.000+09:00</Id>
            <Lap StartTime="2022-09-11T10:39:59.000+09:00">
                <TotalTimeSeconds>1804.0</TotalTimeSeconds>
                <DistanceMeters>195.06</DistanceMeters>
                <Calories>71</Calories>
                <Intensity>Active</Intensity>
                <TriggerMethod>Manual</TriggerMethod>
                <Track>
                    <Trackpoint>
                        <Time>2022-09-11T10:39:59.000+09:00</Time>
                        <Position>
                            <LatitudeDegrees>26.327766180038452</LatitudeDegrees>
                            <LongitudeDegrees>127.75293326377869</LongitudeDegrees>
                        </Position>
                        <AltitudeMeters>26.6</AltitudeMeters>
                        <DistanceMeters>0.0</DistanceMeters>
                        <HeartRateBpm>
                            <Value>79</Value>
                        </HeartRateBpm>
                    </Trackpoint>

            ・・・以下、<Trackpoint></Trackpoint>が沢山続きます・・・

            		 <Trackpoint>
                        <Time>2022-09-11T11:10:04.000+09:00</Time>
                        <Position>
                            <LatitudeDegrees>26.266780853271484</LatitudeDegrees>
                            <LongitudeDegrees>127.72172796726227</LongitudeDegrees>
                        </Position>
                        <AltitudeMeters>16.89</AltitudeMeters>
                        <DistanceMeters>0.0</DistanceMeters>
                        <HeartRateBpm>
                            <Value>96</Value>
                        </HeartRateBpm>
                    </Trackpoint>
                </Track>
            </Lap>
            <Creator xsi:type="Device_t" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
                <UnitId>0</UnitId>
                <ProductID>0</ProductID>
            </Creator>
        </Activity>
    </Activities>
</TrainingCenterDatabase>
```