【Claude】Garmin Venu4のヘルスデータを、Claudeとの協働でNotion×Streamlitに自動蓄積する
背景
スマートウォッチ(Garmin Venu 4)に蓄積されるヘルスデータ(歩数・睡眠・ストレス・HRV・Body Battery・安静時心拍など)を、日々の記録として自動的に手元のデータベースに残したいと考えた。手動でのスクリーンショット管理は却下し、完全自動化を目標に据えた。
構成の全体像は以下の通り。
Garmin Venu 4(デバイス)
→ Garmin Connect(クラウド)
→ 取得スクリプト(cronで1日2回実行)
→ Notion(データ倉庫)
→ 可視化スクリプト(Pandas + Plotly + Streamlit)
以下、構築過程で得られた知見を記録する。
なお、実装作業は Anthropic の Claude を対話相手として進めた。要件を伝えると、Claude が該当する外部APIの現行仕様をその都度Web検索で確認し、スクリプトのコード生成・実行・結果確認・修正、という一連の流れをこちらの手元環境(WSL2)で一歩ずつ検証しながら進める、という協業スタイルをとった。単にコードを書いてもらうだけでなく「なぜ失敗したか」「次に何を確認すべきか」を都度言語化させながら進めた点は、後述する各局面でも繰り返し効いてくる。
1. Garmin Connect への接続方式の選定
Garminのデータを外部から取得する方法は複数存在するが、大きく2系統に分かれる。
(A) Garmin Connect(クラウド)から直接取得する
(B) スマートフォンのヘルスプラットフォーム経由で取得する
今回は WSL2 + Python + cron という既存の実行環境との親和性を優先し、(A) を採用した。(B) は取得できる項目がプラットフォーム標準の範囲に限られ、Garminデバイス独自の指標(Body Batteryなど)が欠落しやすいという制約があったためである。
なお、Garmin非公式APIの認証方式は変遷が激しい領域であり、以前主流だった認証ライブラリは開発終了となっていた。後継のライブラリは、公式モバイルアプリと同様の認証フローに移行し、Cloudflare回避のためTLSフィンガープリント偽装を行うなど、実装がかなり複雑化していた。この手のサードパーティ連携は「今動く」ことと「将来も動く」ことが別問題である点に注意が必要だと感じた。
2. データベース設計(Notion)
データの保存先には Notion API を採用した。理由は、既存の別プロジェクト(ToDo管理)で Notion 連携の知見が既にあったこと、API経由での操作に一貫性を持たせたかったことによる。
確定したスキーマ
最終的に、以下の15項目を1日1レコードとして記録する構成にした。
日付:タイトル。主キー的に使用
歩数 / 距離 / 消費カロリー:活動サマリ由来
睡眠スコア / 睡眠時間 / 仮眠時間 / 総睡眠時間 / 深い睡眠時間:睡眠サマリ由来
ストレス平均:活動サマリ由来
Body Battery(最高 / 最低):活動サマリ由来
安静時心拍:活動サマリ由来
HRV夜間平均:睡眠サマリ由来
取得ステータス:スクリプト側で判定(成功 / 部分 / 失敗)
睡眠時間を「夜間」「仮眠」「合計」に分けたのは、生活リズムが不規則な場合、夜間睡眠と仮眠を合算してしまうと実態が見えなくなるためである。取得元APIが返す睡眠データには、夜間睡眠秒数と仮眠秒数が個別のフィールドとして含まれており、追加の取得コストなしに分割記録が可能だった。
「取得ステータス」列を設けたのは、欠損データを「なかったことにせず、欠損として残す」ためである。自動取得パイプラインは将来的に必ず失敗する箇所が出るため、成功/部分/失敗を明示しておくことで、後から見返した際にデータの信頼度を判断できるようにした。
3. データマッピングの確認方法
実際にどのAPIレスポンスのどのキーに目的の値が入っているかは、公式ドキュメントだけでは分からない部分があった。そのため、対象日のデータを一度APIから取得し、全フィールドをファイルにダンプしてから、目的のキーを目視で特定するという手順を踏んだ。この「まず読み取り専用の探索スクリプトを書いて、実データの構造を確認する」という段取り自体もClaudeが提案したもので、書き込み処理を実装する前に必ず挟むようにした。
推測でキー名をコードに書いて実行し、失敗して初めて気づくよりも、先に実データの構造を確認してからマッピングを確定する方が、結果的に手戻りが少なかった。
この過程で、当初4〜5種類のAPIメソッドを呼ぶ想定だったところ、実際には2種類のメソッドの戻り値だけで15項目すべてを賄えることが判明した。これにより外部APIへのアクセス回数を最小化でき、レート制限を回避しやすい構成になった。
4. 書き込みスクリプトの設計
書き込み処理では、以下の点を重視した。
冪等性:同じ日付のレコードが既に存在する場合は更新、存在しない場合は新規作成とする。これにより、同じスクリプトを1日に何度実行しても、レコードが重複しない設計にした。
Dry-runモード:実際に書き込む前に、変換後のデータ内容だけを表示して確認できるオプションを用意した。本番実行前の確認に有効だった。
欠損の扱い:値が取得できなかった項目は None のまま送信し、当該セルを空欄として残す。数値を仮に0などで埋めてしまうと、後の集計で「本当に0だったのか、取得できなかったのか」が区別できなくなるため、欠損は欠損のまま扱う方針とした。
5. cron による自動化
(補足:cronとは、Linux系OSに標準で備わっている「時刻を指定してプログラムを定期実行させる仕組み」のことである。「毎日10時に◯◯を実行する」のような設定を1行書いておくだけで、OS側が裏で勝手にそのプログラムを起動してくれる。今回は、データ取得スクリプトを1日2回自動で走らせる役目として使った。)
WSL2環境でも systemd を有効化していたため、cronサービス自体は問題なく常駐していた。ただし、cronから直接Pythonスクリプトを実行する際には注意点があった。
cronの実行環境は対話シェルとは異なり、.bashrc などの設定ファイルを自動的に読み込まない。そのため、仮想環境(venv)の有効化や、APIトークンなどの環境変数の設定を、実行対象のスクリプト自身が明示的に行う必要がある。これを怠ると、「手動で実行すると動くが、cron経由だと失敗する」という典型的な事故になる。対策として、環境構築処理と本体スクリプトの呼び出しを分離したラッパースクリプトを用意し、cronからはそのラッパーのみを呼び出す構成にした。
また、生活リズムが不規則な場合、固定の時刻に1回だけ取得すると、睡眠データがまだ確定していない(起床前)タイミングに当たり、欠損レコードが記録されるリスクがある。今回は1日2回(時間帯をずらして)取得する運用にし、冪等性のある書き込み設計と組み合わせることで、片方のタイミングで欠損しても、もう片方で補完される仕組みとした。
なお、WSL2は上位のOS自体がスリープ・シャットダウンしている間は動作しない。そのため、cronで組んだジョブも、その時間帯にマシンが起動していなければ実行されない(後から追いつき実行されるわけでもない)。常時起動に近い運用であればほぼ問題にならないが、環境依存の制約として記録しておく。
このパイプライン全体は外部の生成AIサービス(LLM)を一切経由しない構成のため、実行回数を増やしても追加のAPI利用料が発生しない点も付記しておく。
6. 可視化(Pandas + Plotly + Streamlit)
可視化部分の実装にあたり、後々の機能追加でコードが混沌としないよう、あらかじめ役割を分離した構成にした。
dashboard/
├── app.py # 各層を呼び出すだけの薄い司令塔
├── sources/ # 「どこからデータを読むか」を閉じ込める層
├── transform/ # 「どう加工するか」(Pandas)を閉じ込める層
└── views/ # 「どう表示するか」(Streamlit/Plotly)を閉じ込める層
この分離の意図は、将来データソースが増えた場合(例:別デバイスのデータを追加するなど)に、既存のファイルを一切変更せず、新しいファイルを追加するだけで対応できるようにすることである。表示ロジックの変更が加工ロジックに影響しないようにする、というのも同様の狙いによる。
可視化ライブラリの選定
当初は Streamlit 標準の簡易チャート機能を使っていたが、以下の問題が生じた。
データ点数が少ない状態では、横軸が不自然に細かい時間刻みで表示される
縦軸が自動スケールされ、本来あり得ない負の値の範囲までグラフが伸びる
これらは簡易チャート機能の自動スケーリングの限界であり、軸を明示的に制御できないタイプの実装では解決できなかった。そのため、より詳細な制御が可能な描画ライブラリ(Plotly)に切り替え、以下を明示的に指定する構成にした。
スコア系の指標(0〜100の範囲を持つもの)は縦軸を固定
横軸は日付単位の表示に固定
加えて、閲覧デバイス(タブレット等)を意識し、画面をカテゴリ(睡眠 / 活動 / その他)に分けてラジオボタンで切り替えられるUIにした。1画面に全グラフを並べるより、テーマごとに切り替えて表示する方が、小さい画面での閲覧に適していると判断したためである。
数値表示について
保存しているデータ型(数値)と、人が読みやすい表示形式(例:時間分表記)は、明確に分離した。データベース側のデータは常に数値のまま保持し、「見やすい表記への変換」は表示層でのみ行う方針とした。理由は以下の通り。
データベース側を人間向けの文字列表記にしてしまうと、後で集計・グラフ化ができなくなる
表示の都合はいつでも変更されうるが、データの型を後から直すのはコストが高い
「保存は機械のために、表示は人間のために」という役割分担を最後まで意識した構成にした。
Claudeとの協業について
今回の一連の作業は、Claudeに丸ごと委任するのではなく、各ステップを人間側が実行・確認しながら進める形をとった。Claudeの役割は主に以下の3点に整理できる。
外部APIやライブラリの現行仕様をWeb検索で確認し、実装方針を提示する
要件に沿ったスクリプトを生成し、実行結果(エラーやログ)を踏まえて修正する
トラブルが起きた際に、原因の切り分けと対処の優先順位を言語化する
特に有用だったのは、何かがうまく動かなかったときに「とりあえず別の方法を試す」のではなく、「なぜ失敗したのか」を一次情報(公式ドキュメントや実際のAPIレスポンス)に基づいて確認してから次の手を打つ、という進め方を徹底できた点である。手戻りの多くは、思い込みや古い知識に基づいて実装を進めたときに発生しており、都度検証を挟むことでその大半を未然に防げた。
まとめ
今回の構築を通して得られた要点は以下の通り。
サードパーティAPIの認証方式は変遷するため、実装前に必ず現行仕様を確認する
外部サービスのAPI仕様変更(特にデータモデルの構造変更)は、公式ドキュメントの一次情報で都度検証する
データマッピングは推測せず、実際のレスポンスを確認してから確定する
自動化パイプラインは、失敗を前提に設計する(冪等性・欠損の可視化・ログ記録)
cron等のバッチ実行環境は、対話シェルと環境が異なることを前提に組む
データの「保存形式」と「表示形式」は分離し、将来の機能追加に強い構造にする
いずれも目新しい話ではないが、実際に手を動かしてつまずいた箇所を丁寧に踏むことで、改めて重要性を実感した工程だった。
