LoginSignup
20
22

GCP + Fitbit + Notion でお手軽健康管理

Last updated at Posted at 2024-02-20

Motivation

年末に購入した Google Pixel Watch 2 であるが、それまで使用していた Apple Watch との違いを特に感じず、モヤモヤしていたのだが、Pixel Watch には Fitbit アプリがデフォルトで入っており、睡眠データなどのヘルスデータや歩数などのアクティビティデータを取得してくれるので、それを使って何かしたいと考えた。
Fitbit アプリや web のダッシュボード(https://www.fitbit.com) では各日のデータを確認できるが、複数日にまたがって一覧でまとめてデータを俯瞰したり、他の記録(e.g. 習慣トラッカーなど)と合わせて記録を残したい場合にはそれがなかなか難しいので、それらを全て Notion に突っ込むことで自分だけの活動記録テーブルを作ろうと考えた。

TL;DR

タイトルにあるツール群を用いて、ヘルスデータ(歩数、睡眠時間、ランニング時間など)を自動的に管理下に置くためのパイプラインを敷設した。
具体的には、「データを Fitbit API で取得して、それを Notion API で書き込む」サーバーレス環境を構築した。

前提条件

本パイプライン構築のために必要となるものについて記載する。
今回は Fitbit と Notion を利用しているが、仮に Fitbit ではなく Garmin だろうが、Notion ではなく Google Sheets だろうが、その部分を自分でカスタマイズすれば同様のパイプラインの構築は可能だと思う。

開発環境

  • 開発環境: Mac/MacOS 14.0
  • npm: 9.8.1
  • node: v18.18.0

その他必要なもの

  • Fitbit 対応デバイスでデータを取得していること(今回は Google Pixel Watch2(Wi-fi モデル) を使用)

アーキテクチャ

test.png

大まかな流れは、以下の通り。

  1. Cloud Scheduler(定刻のPub/Sub トリガー起動)
  2. Pub/Sub(Cloud Scheduler からのメッセージを受けて、Cloud Functions 関数をキックする)
  3. Cloud Functions(Fitbit データの取得、Notion へのデータ書き込み)
  4. Firestore(Fitbit API refresh token の保存と取得)

各種セットアップ

ここから実際に手を動かして構築していく部分の説明となる。

各ツール群の詳細な手順とはなっていないため、不明点は別途他の情報に当たって頂きたい。(特に GCP まわりは説明端折ってます)

Fitbit

  • こちらから開発者アカウントを作成する

  • こちらからアプリケーションを新規登録する

    • 色々入力項目があるが、基本的にテキトーで良い。URL は http://localhost:8080/ などにしておいたら良い

  • 登録が完了すると、MANAGE MY APPS ページに OAuth 2.0 Client IDClient Secret が作成されているので確認する

  • こちらからリフレッシュトークンを発行する

    • step 4 で refresh_token を発行したら、それを後で利用するため控えておく1
  • また、こちらで Fitbit のアクティビティデータが入っていることを確認しておくと良いだろう

Notion

  • ページを作成する

    • ページID(https://www.notion.so/ に続く32文字の英数文字列)は後に利用するので控えておく

  • 作成したページ配下でデータベースを作成する

    • データベースID(https://www.notion.so/ に続く32文字の英数文字列)は後に利用するので控えておく

  • 作成したデータベースに対して

    • デフォルトで一番左側のプロパティ(Name という名前で、Type=Title)を Day に変更する
      • Type=Title のプロパティは、データベースにおけるキーとなる
    • 以下のプロパティを追加する
      • RunningTime: その日にランニングした時間(分)
        • Type: Number
      • SleepDuration: 前日からその日にかけての睡眠時間(分)
        • Type: Number
      • Steps: その日の歩数
        • Type: Number

    このようなイメージになる。

  • シークレットを作成する

    • こちらで新規インテグレーションを作成
    • Associated workspaceから作成したページやデータベースが属するワークスペースを選択し、Name に任意の名前を入力して Submit する
    • 作成されたインテグレーションページに行き、Internal Integration Secretの内容を控える

GCP

Cloud Functions

環境変数

必要な以下の項目を環境変数を .env に格納する

本来アクセストークンやそれに準ずる秘匿性の高い情報を環境変数で扱うのはあまりセキュリティ的に良くないので、個人利用以外のプロジェクトの場合は GCP では Secret Managerなどの利用の検討をお勧めする。

COLLECTION_PATH=credentials
FITBIT_DOC_ID=fitbit
NOTION_DOC_ID=notion
CLIENT_ID=XXXXXX
CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NOTION_ACCESS_TOKEN=secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NOTION_PARENT_PAGE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
PUBSUB_TOPIC_NAME=habit-tracker-pubsub-topic # for パラメータ化された構成

ここで、各変数は以下のような意味である:

  • COLLECTION_PATH: Firestore ルートコレクション名
  • FITBIT_DOC_ID: Firestore Fitbit 関連のドキュメント名
  • NOTION_DOC_ID: Firestore Notion 関連のドキュメント名
  • CLIENT_ID: Fitbit OAuth 2.0 Client ID
  • CLIENT_SECRET: Fitbit client secret
  • NOTION_ACCESS_TOKEN: Notion API access token(secrets)
  • NOTION_PARENT_PAGE_ID: Notion ページの Page ID。取得時は 32 文字の英数文字列のため、これを UUID の形式に変換して記述する(適当な位置に-を挿入する)
  • PUBSUB_TOPIC_NAME: Pub/sub トピック名
コーディング

要所を少し紹介(Githubで全ソースコード公開)

  • Fitbit アクティビティデータ取得
    • fitbit.ts > fetchData で、dateForFitbit で指定された日付のデータを取得する
    • sleep.ts > fetchSleepData は Fitbit API2 とのインターフェース
fitbit.ts
import { fetchSleepData } from '@/api/fitbit/sleep';

export const fetchData = async (dateForFitbit: string, accessToken: string) => {
  const sleep = await fetchSleepData(dateForFitbit, accessToken);
  const sleepDurationInMin = sleep.summary.totalMinutesAsleep;
  ...
};
sleep.ts
export const fetchSleepData = async (dateString: string, accessToken: string) => {
  const url = `https://api.fitbit.com/1.2/user/-/sleep/date/${dateString}.json`;
  const response = await fetch(url, {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });
  const data = await response.json();
  return data;
};
  • Notion への書き込み
    • notion.ts > updateDBPage で targetDate で指定された日の記録を更新する
    • page.ts > updatePage は、Notion API とのインターフェース
notion.ts
import { updatePage } from '@/api/notion/page';

export const updateDBPage = async (targetDate: DayjsDate) => {
  // filter pages in database that matches "`day`日" for property 'Day'.
  const accessToken = await issueTokenPair(oldRefreshToken);
  const { ..., sleepDurationInMin } = { ...(await fetchData(dateForFitbit, accessToken)) };
  const day = targetDate.format('D');
  const pageIds = await findPageByDay(day);

  const updatePageData = {
    page_id: pageIds[0],
    properties: {
      ..
      SleepDuration: {
        number: sleepDurationInMin,
      }
    }
  };

  await updatePage(updatePageData, NOTION_ACCESS_TOKEN);
};
page.ts
import { Client } from '@notionhq/client';
import { UpdatePageParameters } from '@notionhq/client/build/src/api-endpoints';

export const updatePage = async (params: UpdatePageParameters, accessToken: string) => {
  const notion = new Client({
    auth: accessToken,
  });

  const response = await notion.pages.update(params);
};
デプロイ

デプロイにより、Pub/Sub トピックは自動生成される

# Firebase の CLI ツールのインストール
npm install -g firebase-tools

# デプロイ(Firebase Cloud Function のプロジェクトを CLI で作成すると、デフォルトでデプロイコマンドがセットされている)
npm run deploy

Firestore コレクション/ドキュメント用意

以下の構造のコレクションを作成する。
このセクションで取得したリフレッシュトークンと、このセクションで取得したデータベースID をあらかじめ入れておく。
これらの情報は初回のみ格納すればよく、以降はプログラム実行時自動更新される。

COLLECTION_PATH # root collection
├── FITBIT_DOC_ID # document
│   └── refreshToken: <xxxxxxxxxxx...>
└── NOTION_DOC_ID # document
    └── databaseId: <xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx>

Cloud Scheduler

定刻に処理実行されるように job 定義(e.g. 10 0 * * * = 毎日0:10)

あとは深夜0:10 を待つのみ!

Cloud Scheduler によりメッセージが Pub/Sub トピックめがけて送信されると、以下の処理が実行される:

  • (毎日)その日のデータ行を一行追加し、前日のデータを書き込む
  • (毎月1日のみ)データベース(テーブル)を追加する

これにより、月替わり対応も自動で行い、基本的にはどんどん新しいデータを書き込んでいってくれるようになっている。

データ書き込みされた Notion 画面イメージ

Fitbit のデータが書き込むのと併せて、習慣トラッカーの項目もチェックボックスで組み込んでいる。
現状はこれらは手でぽちぽちやっているが、これも API 経由で操作可能なので、何かの判断材料をもとに自動で記録することができる。

image.png

Notion では各プロパティは Type に基づいて集計関数が用意されているため、Number 型データの場合、平均値や合計値などをテーブル下方に表示することができる。
また、実際に時間に関するデータは単なる分表記ではなく「HH時間MM分」などの形式にした方が可読性は高いが、その変換を Cloud Functions 側で行った結果を Notion に書き込む場合 Type=Text となるため、上述の集計関数に制限が出てしまうのがモヤモヤポイントだ(平均睡眠時間などが出せない)。

所感

  • Notion はシステムインテグレーション周りが非常によく整備されていて、API エンドポイントを直たたきせずとも SDK が用意されており、型補完を効かせることができて開発しやすい
  • データ挿入位置がデータベース上意図しない場所になされるため、順番に挿入されるようにしたい
  • Fitbit 以外のデータを連携してみて何か面白い示唆が現れるかをみてみたい
  1. 初回のみリフレッシュトークンを手動で取得することで、OAuth 認証認可プロセスにおけるユーザーの同意を得るプロセスをスキップしている。一度リフレッシュトークンが手に入れば、あとは API 経由でトークンを再発行すれば良い。

  2. 本エンドポイントの詳細はこちら: https://dev.fitbit.com/build/reference/web-api/activity/get-daily-activity-summary/

20
22
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
20
22