My heart beats for U —— 心拍数同期 Grafana 表示

4 min

ちょっとした試みで、Apple ヘルスの心拍数を定期的にサーバーへ同期し、Grafana で表示する仕組みを作りました。結果はこんな感じです:

ブログ右上の ♥️ アイコンをクリックすると見られますが、Cloudflare Tunnel を使っているため国内からのアクセスは遅いので、できれば VPN を使ってアクセスしてください。 現在は停止中です。Oppo スマホに変えたため、心拍数の同期アップロードができなくなりました。

大まかな流れは、こちらのアプリ Health Auto Export - JSON+CSV の Restful API 機能を使い、心拍数データを定期的に指定の HTTP インターフェースへ送信して InfluxDB に書き込み、Grafana が InfluxDB に接続してダッシュボードを描画するというものです。

Health Auto Export - JSON+CSV は定期同期を使うには Premium プランが必要で、米国ストアの Lifetime は 24.99 USD と少し高めですが、代替案はあまりなさそうです。

購読後、新しく Automation を作成します:

  • Automation Type は REST API
  • URL は後述のサービスのアドレスで、API パスは /push/heart_rate
  • Data Type は Health Metrics
  • Select Health Metrics で Heart Rate にチェック​
  • Export Format は JSON を選択
  • Sync Cadence は 1 分または 5 分を選べますが、Apple Watch は常時心拍を測定しているわけではありません

Enable にチェックを入れれば OK。アプリ終了後も同期を続けるために、デスクトップウィジェットを追加しておくと良いです。

次に、Restful API のエンドポイントを公開するサービスをデプロイし、データを受け取って InfluxDB に書き込みます。InfluxDB の導入は各自で調べてください。InfluxDB はバージョン 2 を使っています。

サービスのソースコードは reekystive/healthkit-collector にあり、Node.js プロジェクトです。pnpm で起動すると 3000 番ポートで待ち受けます。私は Dockerfile を書いて Docker イメージ化し、自宅サーバーにデプロイしました。

FROM node:20-alpine AS builder

# pnpm をインストール
RUN corepack enable && corepack prepare pnpm@9.14.2 --activate

# 作業ディレクトリ設定
WORKDIR /app

# package.json と pnpm-lock.yaml をコピー
COPY package.json pnpm-lock.yaml* ./

# 依存関係をインストール
RUN pnpm install --frozen-lockfile

# ソースコードをコピー
COPY . .

# アプリケーションをビルド
RUN pnpm build

# Stage 2: 本番用ステージ
FROM node:20-alpine AS production

# pnpm をインストール
RUN corepack enable && corepack prepare pnpm@9.14.2 --activate
FROM node:20-alpine AS builder

# pnpm をインストール
RUN corepack enable && corepack prepare pnpm@9.14.2 --activate

# 作業ディレクトリ設定
WORKDIR /app

# package.json と pnpm-lock.yaml をコピー
COPY package.json pnpm-lock.yaml* ./

# 依存関係をインストール
RUN pnpm install --frozen-lockfile

# ソースコードをコピー
COPY . .

# アプリケーションをビルド
RUN pnpm build

# Stage 2: 本番用ステージ
FROM node:20-alpine AS production

# pnpm をインストール
RUN corepack enable && corepack prepare pnpm@9.14.2 --activate

# 作業ディレクトリ設定
WORKDIR /app

# package.json と pnpm-lock.yaml をコピー
COPY package.json pnpm-lock.yaml* ./

# 本番用依存関係のみインストール
RUN pnpm install --prod --frozen-lockfile

# builder ステージからビルド済みアプリをコピー
COPY --from=builder /app/dist ./dist

# 環境変数設定(デフォルト値。実行時に上書き可能)
ENV NODE_ENV=production
ENV PORT=3000

# アプリが使用するポートを公開
EXPOSE ${PORT}

# アプリケーション起動コマンド
CMD ["node", "dist/index.js"]

起動時には InfluxDB 接続用の環境変数を 4 つ設定します。

INFLUXDB_TOKEN='your_influxdb_token'
INFLUXDB_URL='your_influxdb_url'
INFLUXDB_ORG='your_influxdb_org'
INFLUXDB_BUCKET='your_influxdb_bucket'

デプロイ完了後、一度同期を試みると、サービス側で DB 書き込み成功のログが出ます。

最後に Grafana でダッシュボードを作成します。Data Source を追加し、新規ダッシュボードで以下のクエリを使います。

from(bucket: "bpm")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "heart_rate")
  |> filter(fn: (r) => r["_field"] == "avg" or r["_field"] == "max" or r["_field"] == "min")

ぜひお楽しみください!