最近 Fitbit つけて散歩してるので CircleCI + Pixela で見えるようにしてみた

歩数で草を生やしてみたのだー。これで散歩の楽しみが増えたなー

https://pixe.la/v1/users/bufferings/graphs/steps

やってること

やってることはシンプルで、こう↓

  1. Fitbit の API で歩数を取得
  2. Pixela に記録
  3. というスクリプトをつくって
  4. CircleCI で定期的に実行

Fitbit?

最近 Fitbit Sense というスマートウォッチをつけて散歩してる

www.fitbit.com

会社の福利厚生で健康のための補助があるので、それを利用して手に入れたのだ。散歩やジョギングをしたら勝手に記録されてるので便利。睡眠のログも取られてるので面白い

Pixela?

Pixela は日々の活動を記録して見えるようにしてくれる API サービス。いちばん最初に貼った画像みたいに GitHub の草を生やすやつみたいにしてくれる。操作が全部 API 経由なのも楽しい

pixe.la

せっかく散歩してるから

せっかく散歩してるから Fitbit の歩数データを Pixela で見えるようにしたら面白いかなぁって思ったのだった。何か参考にできる記事あるかなぁ?って思ってたら、ようへいさんのブログに参考というかまさにそれという記事がありました

inokara.hateblo.jp

そして気づく。そうか、こういうのに CircleCI 使えばいいのか! CircleCI で仕事してるのに全然頭になかったー(おーい

じゃ、やってみよう

1. Fitbit から API で歩数を取得

やるのはこういうこと

  • Fitbit にアプリを登録してアクセストークンとリフレッシュトークンを取得
  • このアクセストークンを使って API を叩くと情報が取得できる

んだけど

  • アクセストークンの有効期限は8時間なので、リフレッシュトークンを使って再度アクセストークンとリフレッシュトークンを取得する必要がある

という感じ。ちょっとややこしいけど、健康状態の記録を扱うんだからちゃんとしてるのは良いね

Fitbit にアプリを登録してトークンを取得

API トークンみたいなのあるのかなぁ?と思ったけど、ないみたい?で、アプリを登録する必要がある。アプリといっても、自分で API を叩くためのトークンを取得するためだけなので URL は localhost にしておいたら大丈夫。この記事がとても丁寧で分かりやすかった↓

これで、アクセストークンとリフレッシュトークンが取得できた。それと Authorization: Basic のヘッダーのところのトークンも API を叩くときに使うのでメモしておく(これは Client ID とかから自分で生成できるんだけど)

Fitbit の API をたたいてみる

この API を使う

こんな感じで過去7日分の歩数を取得することができる:

❯ FITBIT_ACCESS_TOKEN="さっき取得したアクセストークン"

❯ curl -sX GET "https://api.fitbit.com/1/user/-/activities/steps/date/today/7d.json" \
     -H "accept: application/json" \
     -H "authorization: Bearer $FITBIT_ACCESS_TOKEN" \
     | jq
{
  "activities-steps": [
    {
      "dateTime": "2022-02-21",
      "value": "7204"
    },
    {
      "dateTime": "2022-02-22",
      "value": "7485"
    },
    {
      "dateTime": "2022-02-23",
      "value": "1014"
    },
    {
      "dateTime": "2022-02-24",
      "value": "1420"
    },
    {
      "dateTime": "2022-02-25",
      "value": "3615"
    },
    {
      "dateTime": "2022-02-26",
      "value": "2599"
    },
    {
      "dateTime": "2022-02-27",
      "value": "366"
    }
  ]
}

さっき取得したアクセストークンの有効期限は8時間なことに注意。なので、ずっとデータを取得し続けるためには、リフレッシュトークンを使って定期的にトークンを再生成する必要がある。やってみよう

リフレッシュトークンでトークンを再生成する

使うのは、この API

❯ FITBIT_BASIC_TOKEN="さっき取得した Authorization: Basic のところのトークン"
❯ FITBIT_REFRESH_TOKEN="さっき取得したリフレッシュトークン"

❯ curl -X POST "https://api.fitbit.com/oauth2/token" \
     -H "accept: application/json" \
     -H "authorization: Basic $FITBIT_BASIC_TOKEN" \
     -d "grant_type=refresh_token&refresh_token=$FITBIT_REFRESH_TOKEN"
     | jq
{
  "access_token": "新しいアクセストークン",
  "expires_in": 28800,
  "refresh_token": "新しいリフレッシュトークン",
  ...
}

という感じで、新しいアクセストークンとリフレッシュトークンを取得できる。再生成すると古い方のアクセストークンとリフレッシュトークンは使えなくなることに注意

こんな風にしてリフレッシュトークンを再生成し続ければ、ずっとアクセストークンを取得できそうなので、これで Fitbit のデータ取得は OK かな

2. Pixela に記録

じゃ、取得した歩数データを Pixela に登録しよう。Pixela は日本語ドキュメントがあるので、その通りにやってくだけ

グラフはこんな感じで作った

❯ PIXELA_TOKEN="Pixela に登録したトークン"

❯ curl -X POST https://pixe.la/v1/users/bufferings/graphs \
     -H "X-USER-TOKEN:$PIXELA_TOKEN" \
     -d '{"id":"steps","name":"steps","unit":"steps","type":"int","color":"ajisai","timezone":"Asia/Tokyo"}'

{"message":"Success.","isSuccess":true}

記録もシンプルで、日付をパスパラメータで指定して値を PUT で渡してあげればいい

❯ curl -X PUT https://pixe.la/v1/users/bufferings/graphs/steps/20220221 \
     -H 'x-user-token: '$PIXELA_TOKEN \
     -d '{"quantity":"7204"}'

{"message":"Success.","isSuccess":true}

これで Pixela に歩数を記録することができるようになったーわーい

3. というスクリプトを作成

TypeScript で書いた

https://github.com/bufferings/fitbit-to-pixela/blob/main/src/main.ts

const main = async () => {
  const [accessToken, newRefreshToken] = await fetchNewTokensFromFitbit();
  await saveNewRefreshToken(newRefreshToken);

  const stepsList = await fetchStepsFromFitbit(accessToken);
  for (const steps of stepsList) {
    await putToPixela(steps);
  }
};

やってることはこれだけ:

  1. リフレッシュトークンを使って新しいアクセストークンとリフレッシュトークンを取得
  2. 新しいリフレッシュトークンを環境変数に保存
  3. 新しいアクセストークンを使って過去7日分の歩数を Fitbit から取得
  4. 7日分の歩数を Pixela に登録

3-1. リフレッシュトークンを使って新しいアクセストークンとリフレッシュトークンを取得

これはさっき手でやってみたやつをコードにしたもの。もしかしたらアクセストークンはまだ有効期限内かもしれないんだけど、1日に数回しか実行しないと思うので、実行するたびに再生成することにした。だから、アクセストークンはどこにも保存してない

3-2. 新しいリフレッシュトークンを環境変数に保存

ローカルでは .env ファイルを使ってこんな風に環境変数を定義してる(トークンなので .gitignore に追加してあって Git にはコミットしていない。トークンをコミットしないようにね!)

IS_LOCAL=true
FITBIT_BASIC_TOKEN="さっき取得した Authorization: Basic のところのトークン"
FITBIT_REFRESH_TOKEN="さっき取得したリフレッシュトークン"
PIXELA_TOKEN="Pixela のトークン"
PIXELA_GRAPH_URL=https://pixe.la/v1/users/(ユーザーID)/graphs/(グラフID)

この FITBIT_REFRESH_TOKEN を使ってアクセストークンとリフレッシュトークンを再生成すると、元のリフレッシュトークンは使えなくなるので次回実行時のために .env ファイルの FITBIT_REFRESH_TOKEN を書き換えてる(ちなみにローカル用と CI 用で別のトークンを使うことができるようにふたつのアプリを Fitbit に登録しといた)

ローカルでは .env の書き換えをしてるんだけど、CircleCI の場合は API を使ってプロジェクトの環境変数を更新してるので後述する

3-3. 新しいアクセストークンを使って過去7日分の歩数を Fitbit から取得

これもさっきやったやつをコードにしただけ

3-4. 7日分の歩数を Pixela に登録

昨日と今日の分だけ記録したら十分なんだけど、まぁあんまり深く考えずに7日分更新することにした。ちなみに、ローカルでいちど 3m を指定して3ヶ月分とってきて更新しといた

4. CircleCI で定期的に実行

色々準備できたので、最後にこのスクリプトを CircleCI で定期実行する

  1. プロジェクトの準備
  2. 新しいリフレッシュトークンを環境変数に保存
  3. 環境変数の登録
  4. 定期実行

4-1. プロジェクトの準備

node の orbs を使って npm run main を実行する設定ファイルを追加。Orbs が色々面倒みてくれるので楽:

version: 2.1

orbs:
  node: circleci/node@5.0.0

workflows:
  fitbit-steps-to-pixela:
    jobs:
      - node/run:
          version: '17.6.0'
          npm-run: main

4-2. 新しいリフレッシュトークンを環境変数に保存

さっきローカルの .env を更新したやつの CircleCI バージョン。この API を使う:

API を使うために、トークンを取得する必要があるので

User Settings > Personal API Tokens から Pixela 用にトークンを生成した

f:id:bufferings:20220227144443p:plain

あとはそのトークンを使って、こんな風にして登録・更新をすることができる

❯ MY_CIRCLE_PROJECT_SLUG="プロジェクトのパス。例: github/bufferings/fitbit-to-pixela"
❯ MY_CIRCLE_API_TOKEN="生成したトークン"

❯ curl --request POST \
     --url "https://circleci.com/api/v2/project/${MY_CIRCLE_PROJECT_SLUG}/envvar" \
     --header "circle-token: $MY_CIRCLE_API_TOKEN" \
     --header 'content-type: application/json' \
     --data '{"name":"foo","value":"xxxx12345"}'

{
  "name" : "foo",
  "value" : "xxxx2345"
}

この API を使って、新しいリフレッシュトークンで FITBIT_REFRESH_TOKEN 環境変数を更新してる

4-3. 環境変数の登録

こんな感じ:

f:id:bufferings:20220227142757p:plain

  • Fitbit 用
    • FITBIT_BASIC_TOKEN
    • FITBIT_REFRESH_TOKEN
  • Pixela 用
    • PIXELA_GRAPH_URL
    • PIXELA_TOKEN
  • CircleCI 用
    • MY_CIRCLE_API_TOKEN
    • MY_CIRCLE_PROJECT_SLUG

4-4. 定期実行

適当に実行して動いてるなぁってことが確認できたら、あとはパイプラインの定期実行トリガーを設定

Project Settings > Triggers で日本時間の0時、6時、12時、18時に実行するようにしてみた。1日に1回でも良い気はするけどなんとなく

f:id:bufferings:20220227145234p:plain

実行されてるみたいでよかった。もうちょっと時間かかるかなぁって思ってたけど 10s くらいか。さくっとしてて良いー

f:id:bufferings:20220227145442p:plain

ということで

歩くと草が生えるー!

https://pixe.la/v1/users/bufferings/graphs/steps

やってることはシンプルなんだけど、Fitbit のアプリ周りのところを理解するのとか TS のプロジェクト作ったりするのに時間がかかってしまったなぁ。それが楽しかった!

散歩してこー!