Fitbit Web APIで生体情報を取得


# 始めに

私は普段からFitbitを愛用しています。（愛機は[Fitbit Versa 4](https://www.fitbit.com/global/jp/products/smartwatches/versa4?sku=523BKBK)）

:::note info
Fitbitは健康とフィットネスを追跡するためのデバイスとサービスを提供する会社です。
Fitbitの製品ラインアップには、アクティビティトラッカーやスマートウォッチがあり、歩数、心拍数、睡眠の質、消費カロリーなどのデータを専用のデバイスで取得し管理しています。
:::

https://www.fitbit.com/global/jp/home

このFitbitのアクティビティデータを使ってなにか出来ないかと思い、まずはAPIからのデータ取得方法の手順を調べたのでメモします。


# Fitbit開発者アカウントの作成

https://dev.fitbit.com/build/reference/web-api/developer-guide/getting-started/


簡単に流れを説明すると以下の流れです。

1. [https://accounts.fitbit.com/signup](https://accounts.fitbit.com/signup)にアクセスして、fitbit.comアカウントの登録
2. Fitbit.comのアカウントが登録できたら、[https://dev.fitbit.com/apps](https://dev.fitbit.com/apps)へアクセスしてAIPを利用するアプリケーションを作成

# ダッシュボードへログイン

https://accounts.fitbit.com/signup

Fitbit.comのアカウントでダッシュボードにログインできることを確認。


# dev.fitbit.comへログイン

https://dev.fitbit.com/apps

Fitbit.comと同じアカウントでdev.fitbit.comへログインできることを確認。

![スクリーンショット 2024-06-13 6.51.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/7c5cd732-f8d0-df80-a5f8-2b742087925c.png)

# アプリケーションを作成

「Register a new app」をクリック

![スクリーンショット 2024-06-13 6.51.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/0db058dc-e574-c531-37be-4a5a82afe240.png)

とりあえず、「Redirect URL」以外のURL入力欄は`http://localhost`と入力しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/ebd57b87-f934-df28-c8a4-ee8d7e0ee67d.png)

「I have read and agree to the terms of service」をチェックしてピンクの「Register」ボタンを押すと、以下のページが表示されます。

![スクリーンショット 2024-06-14 12.50.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/6db01827-59c7-4687-1dff-efa8577161cb.png)

左下の「OAuth2.0 Tutorial」をクリックして、アクセス・トークン等の発行を行います。

## App Settings

「Client ID」は自動的に表示されているのでそのままにしておきます。

「Application Type」は「Client (for client-side access)」を選択します。

![スクリーンショット 2024-06-14 12.53.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/282f7c11-77d7-018a-93d2-70fd585672c4.png)


## Getting an Access Token

### Step 1: Generate PKCE and State Values

アクセス・トークンを発行するために必要な情報を生成します。
「Generate」ボタンを押します。（２箇所）

生成される文字列は一時的なものなので控えておく必要はありません。

![スクリーンショット 2024-06-14 12.56.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/b5c984af-3e76-ff2c-680a-39a6a5a46e6e.png)


### Step 2: Display Authorization Page

APIで取得を認める項目の選択です。
デフォルトでは全てにチェックが入っているので取得を認めない項目からチェックを外します。

>The permission scopes to request access to. Only request what your app needs.

アクセスを要求する権限のスコープ。アプリに必要なものだけをリクエストしてください。

![スクリーンショット 2024-06-14 13.19.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/876c6872-4f78-2aa9-8026-59fb382cbe83.png)


権限のチェックができたら「Authorization URL」をクリックします。

自動で認可ページが開きます。
「すべて許可する」をチェックし、「許可」ボタンを押します。

![スクリーンショット 2024-06-13 8.35.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/c04dacf7-bf93-2313-817e-2fc21819e6a2.png)

すると、別タブでRedirect URLで指定した`http://localhost:8000/~`が開き「このサイトにアクセス出来ません」と表示されていると思います。
**閉じずにURLをコピー** してください。

![スクリーンショット 2024-06-13 8.36.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/b97fd144-805f-4c34-1850-0f3e602adec5.png)

### Step 3: Handle the Redirect

先ほどのページに戻り、下の画像の赤枠のところにコピーしたURLを貼り付けます。
URLからAuthorization CodeとStateを抽出して表示してくれます。
いずれの値も控えておく必要はありません。

![スクリーンショット 2024-06-14 13.00.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/6422d969-95e0-5dff-2009-85a458110a7f.png)

### Step 4: Get Tokens

上記のURLを元にリクエストを自動生成してくれています。
「SUBMIT REQUEST」のボタンを押します。
下のテキストエリアにエラーが表示されていなければOKです。

![スクリーンショット 2024-06-14 13.09.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/6ce7d474-0856-0400-74d7-e0d1c11bc62d.png)


**Access TokenとRefresh Tokenは必ず控えてください。**

Access TokenはAPIを実行するのに使います。
このTokenは8時間の寿命なので、期限が切れた場合はRefresh Tokenを使ってAccess Tokenの再発行を行います。

:::note alert
Access TokenとRefresh Tokenは人に見せないようにしてください。
Access TokenまたはRefresh Tokenが流出した可能性がある場合、ここで作成したアプリを削除してください。
:::

### Step 5: Check Scopes

:::note info
この後のステップは、profile取得のAPIが通るかのテストとRefresh Tokenを使って再発行をするチュートリアルなので、不要なら飛ばしてください。
Profileはそもそも権限を与えていない場合はエラーになるので、試す場合はご留意ください。
また、Refresh Tokenを使って再発行をすると、Access TokenとRefresh Tokenともに更新されるので、更新後の値を控えておいてください。
:::

**Access User Data**

これまでの手順で作成したAceess　TokenとAPI Endpoint　URLが自動でセットされているので、「SUBMITREQUEST」を押します。
APIエンドポイントからユーザープロファイルのデータが取得され「Respose」欄に表示されればOKです。

![スクリーンショット 2024-06-14 13.13.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/57889f44-0f3c-e45b-5cbe-2a76111120be.png)

**Refresh Tokens**

Access Tokenは8時間の寿命なので、期限が切れた場合はRefresh Tokenを使ってAccess Tokenの再発行を行います。

Refresh Tokenが自動でセットされているので、「SUBMITREQUEST」を押します。
Refresh Tokenを用いて、Access Tokenが更新されました。（古いAccess Tokenは使えなくなります）

![スクリーンショット 2024-06-14 13.16.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/5d8e7d53-7b16-4181-0fcf-f2db4829c4c0.png)

# NodejsからAPIを叩いて情報を取得

さて、本題のFitbitAPIでデータ取得をするアプリケーションを自作します。
今回はNode.jsでFitbit APIを利用して「Get Heart Rate Time Series by Date」を取得する簡単なサーバーサイドアプリケーションを作成します。

## Node.jsプロジェクトの初期設定

app.jsを作成します。

```bash
mkdir fitbit-nodejs
cd fitbit-nodejs
npm init -y
```

## 必要なパッケージのインストール


```bash
npm install axios dotenv
```

## 環境変数の設定

```.env
CLIENT_ID=
ACCESS_TOKEN=
REFRESH_TOKEN=
```

いずれもこれまでで取得した値で埋めてください。
client_idはアプリの[登録ページ](https://dev.fitbit.com/apps)で、登録したアプリをクリックすると確認できます。

## データ取得スクリプトの作成

プロジェクトのルートにindex.jsファイルを作成し、以下のコードを追加します。これにより、指定された日付に基づいて心拍数データを取得します。

```javascript
require('dotenv').config();
const axios = require('axios');
const fs = require('fs');
const path = require('path');

async function refreshAccessToken() {
    const url = 'https://api.fitbit.com/oauth2/token';
    const clientId = process.env.CLIENT_ID;
    const refreshToken = process.env.REFRESH_TOKEN;

    const body = new URLSearchParams();
    body.append('grant_type', 'refresh_token');
    body.append('client_id', clientId);
    body.append('refresh_token', refreshToken);

    const config = {
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    };

    try {
        const response = await axios.post(url, body.toString(), config);
        const { access_token, refresh_token } = response.data;

        updateEnvFile(clientId, access_token, refresh_token);
        console.log('Access token and refresh token updated.');
        return access_token;
    } catch (error) {
        console.error('Error refreshing token:', error.response.data);
    }
}

function updateEnvFile(clientId, accessToken, refreshToken) {
    const envPath = path.join(__dirname, '.env');
    const envContents = `CLIENT_ID=${clientId}\nACCESS_TOKEN=${accessToken}\nREFRESH_TOKEN=${refreshToken}\n`;

    fs.writeFileSync(envPath, envContents, 'utf8');
    console.log('.env file has been updated with new tokens.');
}

async function getHeartRate(date = 'today', period = '1d') {
    const accessToken = await refreshAccessToken();
    const url = `https://api.fitbit.com/1/user/-/activities/heart/date/${date}/${period}.json`;

    try {
        const response = await axios.get(url, {
            headers: {
                'Authorization': `Bearer ${accessToken}`
            }
        });
        return response.data;
    } catch (error) {
        console.error('Error fetching heart rate data:', error.response.data);
    }
}

(async () => {
  const heartRateData = await getHeartRate();  // 今日の1日分の心拍データを取得
  console.log(JSON.stringify(heartRateData));
})();

```

## 取得結果


```json
{
	"activities-heart": [
		{
			"dateTime": "2024-06-13",
			"value": {
				"customHeartRateZones": [],
				"heartRateZones": [
					{
						"caloriesOut": 2111.9742600000004,
						"max": 109,
						"min": 30,
						"minutes": 1427,
						"name": "Out of Range"
					},
					{
						"caloriesOut": 88.36310999999998,
						"max": 133,
						"min": 109,
						"minutes": 13,
						"name": "Fat Burn"
					},
					{
						"caloriesOut": 0,
						"max": 163,
						"min": 133,
						"minutes": 0,
						"name": "Cardio"
					},
					{
						"caloriesOut": 0,
						"max": 220,
						"min": 163,
						"minutes": 0,
						"name": "Peak"
					}
				],
				"restingHeartRate": 62
			}
		}
	],
	"activities-heart-intraday": {
		"dataset": [
			{
				"time": "00:00:00",
				"value": 62
			},
			{
				"time": "00:01:00",
				"value": 61
			},
...長いので割愛
			{
				"time": "16:34:00",
				"value": 78
			},
			{
				"time": "16:35:00",
				"value": 74
			},
			{
				"time": "16:36:00",
				"value": 76
			},
			{
				"time": "16:37:00",
				"value": 76
			},
			{
				"time": "16:38:00",
				"value": 80
			}
		],
		"datasetInterval": 1,
		"datasetType": "minute"
	}
}
```

# 最後に

APIからデータ取得ができたので、なにをしようかな。これから考えます。


:::note warn
この記事は個人の見解であり所属する組織を代表しません。
:::