← health-moniter 目次へサイトトップ

HM-004

health-moniter データ取得APIとAI解説

作成: 2026-06-30 / 更新: 2026-07-04 / 対象: Open-Meteo・SwitchBot・Withings・Anthropic / OpenAI API

要約。 気圧・外気温・外湿度はOpen-Meteoの1時間値、室内温湿度はSwitchBot、睡眠・体重はWithingsから取得し、日次集約・24時間気圧変化・Pearson/Spearman相関はhealth-moniterのWorkerで計算しています(いずれも実装済み)。収集はアプリを開いたときのオンデマンド同期に加え、毎時0分のCronで室内温湿度と気象を補充します。AI解説も実装済みで、外部AI(Claude / OpenAI)へ送るのはWorkerが計算した相関統計だけ。生データ・個別記録・メモ・位置情報は送りません。
サービス用途状態認証
Open-Meteo地点、気圧、外気温、外湿度実装済みAPIキー不要
SwitchBot API v1.1室温、室湿度の現在値(毎時Cronで蓄積)実装済みToken + SecretによるHMAC-SHA256
Withings Health Data API睡眠、体重実装済みOAuth 2.0
Anthropic Messages / OpenAI Chat CompletionsWorker解析結果(相関統計)をAIが日本語で解説実装済みAPIキーはWorker Secret(プロバイダーを直接呼び出し)

1. 全体データフロー

外部APIからWorker解析とAI解説までのデータフロー Open-Meteo気圧・外気温・外湿度 SwitchBot室内温湿度 Withings睡眠・体重 Cloudflare Worker同期+毎時CronTypeScript / Hono D1時系列の正本 Worker解析直前24h特徴量日次集約・相関・p値 AI解説Claude / OpenAI 直接相関統計だけ送信 外部AIへ送らないもの 時間別生データ・個別症状記録・メモ・位置情報
図1. 生データはD1とWorker内に留め、Claude/OpenAIには再現可能な相関統計だけを直接送る。SwitchBot/Withings/AIは実装済み。

2. 収集アーキテクチャ: オンデマンド同期+毎時Cron

実装済み。 データ収集は2系統。(1) アプリを開いたときのオンデマンド同期、(2) 毎時0分のCloudflare Cron Trigger(wrangler.jsonctriggers.crons = ["0 * * * *"]worker/index.tsscheduledハンドラ)。Cronは室内温湿度の現在値ポーリングと気象のトップアップを実行し、片方の失敗がもう片方を止めないようPromise.allSettledで分離しています。

なぜCronが要るか — SwitchBotに履歴APIがない

SwitchBot公開API v1.1が返すのは現在値だけで、過去履歴を取るエンドポイントが存在しません(公開されているのはデバイス一覧・現在ステータス・コマンド送信・シーン一覧・シーン実行の5種類のみ)。したがって室内温湿度を時系列として蓄積する唯一の手段が定期ポーリングです。毎時Cronが室内温湿度を1点取り、気象は取りこぼしを補充します(Withingsはアプリ起動時同期のまま)。

収集経路きっかけ取るもの
オンデマンド同期アプリを開いたとき気象・SwitchBot・Withings(睡眠/体重)を裏で同期
毎時Cron毎時0分(Workers Cron Trigger)SwitchBot室内温湿度の現在値+気象トップアップ

2段階ロード — 保存済みを即表示し、同期は裏で回す

2段階ロードのシーケンス 開いた瞬間 時間の経過 → 1. 即描画 D1の保存済みデータを そのまま表示(待たない) 画面はブロックされない 2. 裏で同期 気象 / SwitchBot / Withings をバックグラウンド実行 画面に「…同期中」表示 3. 再取得 同期完了後に自動で 最新データを再取得・再描画 最新化される
図2. 2段階ロード。保存済みを即表示してから外部同期を裏で回し、完了後に再取得する。

フロントは開いた瞬間にD1の保存済みデータを描画し、3つの外部同期(気象・SwitchBot・Withings)はバックグラウンドで実行して「…同期中」と表示します。同期完了後に自動で再取得して最新化するため、上流が遅くても最初の描画はブロックされません。

10秒タイムアウトと502 — Open-Meteo Forecastの単一オリジン問題

収集トリガと10秒タイムアウト 毎時Cron crons: "0 * * * *" アプリ起動 オンデマンド同期 Worker scheduled / fetch 10秒タイムアウト AbortSignal.timeout (10_000) 外部API Open-Meteo SwitchBot / Withings 502 UPSTREAM_ERROR 失敗・無応答
図3. 収集の2トリガ。すべての外部fetchに10秒の上限を課し、超過・失敗は502へマップする。

外部API(Open-Meteo / SwitchBot / Withings)への全fetchにAbortSignal.timeout(10_000)を設定。タイムアウトやネットワーク失敗は上流障害として502(UPSTREAM_ERROR)にマップします(内部エラー500ではなく)。

実測でわかったこと。 api.open-meteo.com(Forecast)はドイツHetznerの単一オリジン(CDNなし)で、間欠的にTCPレベルで無応答になります。同時刻にarchive-apigeocoding-apiは正常応答したことから、Forecastホスト固有の問題と特定しました。これが「アプリが時々すごく遅い」の原因で、タイムアウト+2段階ロードで画面のブロックを解消しています。

3. 気象データ: Open-Meteo

実装済み。 実装はhealth-moniterのfrontend/worker/open-meteo.ts。時間はUTCで取得し、D1へ保存します。

実際に使っているURL

目的URL使い分け
直近期間の時間別気象https://api.open-meteo.com/v1/forecast同期終了日の5日前以降
それより古い時間別気象https://archive-api.open-meteo.com/v1/archive過去の欠損期間
地名検索https://geocoding-api.open-meteo.com/v1/search地名から緯度・経度・タイムゾーン候補を取得

時間別気象リクエストの主要パラメータは次のとおりです。日付と緯度経度は保存済み地点と同期範囲からWorkerが埋めます。

latitude={lat}
longitude={lon}
hourly=surface_pressure,temperature_2m,relative_humidity_2m
timezone=UTC
start_date=YYYY-MM-DD
end_date=YYYY-MM-DD

札幌市の座標を使った形式例です。日付指定を省略したため、リンクを開いた時点のForecast API既定期間が返ります。

https://api.open-meteo.com/v1/forecast?latitude=43.06667&longitude=141.35&hourly=surface_pressure,temperature_2m,relative_humidity_2m&timezone=UTC

バックフィルの窓とウォームアップ

実装済み。 POST /api/sync/weatherと毎時Cronは同期カーソル以降を保存し、直近7日より過去へは遡りません(それ以前は保存済みとみなす)。Forecast APIは最新5日ぶんだけを担当し、それより古い欠損はArchive(再解析)で埋めます。24時間気圧変化は各行のt−24hの値が要るため、保存範囲の1日前からウォームアップ取得しますが、ウォームアップ行自体は保存しません。以前はウォームアップ行をnullで書き戻して既存のpressure_change_24hを消すバグがあり、本番122行中120行がnullになっていました(修正済み)。

取得値とWorkerで作る値

health-moniterの値由来処理
気圧surface_pressure(hPa)Open-Meteoの地表面気圧。海面更正気圧pressure_mslではない
外気温temperature_2m(℃)地上2mの1時間値
外湿度relative_humidity_2m(%)地上2mの1時間値
24時間気圧変化Worker計算現在時刻の気圧 − 24時間前の気圧
日次平均気温・気圧・湿度Worker計算地点タイムゾーンの同じ日付に属する1時間値を平均
集約の実装。 相関ダッシュボードとAI解説は、地点タイムゾーンの現地暦日で時間値を日次平均し、同じ日付の複数記録を1つの解析点へ集約します(記録回数による過剰な重み付けを避けるため)。唯一の例外が「起床からの経過時間(h)」で、これだけは日次平均せず記録1件単位で相関を取ります(詳細は第6節)。

4. 室内環境: SwitchBot API v1.1

実装済み。 現在の室温・室湿度を1点取得します(worker/switchbot.ts)。公開APIに履歴取得がないため、毎時Cronのポーリングで時系列を蓄積します(第2節「収集アーキテクチャ」参照)。fetchは10秒でタイムアウトし、失敗時は502。
目的メソッド・URL結果
デバイス一覧GET https://api.switch-bot.com/v1.1/devices温湿度計のdeviceIdを特定
現在状態GET https://api.switch-bot.com/v1.1/devices/{deviceId}/status機種が返す現在の温度・湿度

認証ヘッダーはAuthorizationsigntnonceです。WorkerのWeb Cryptoでtoken + timestamp + nonceをSecretによるHMAC-SHA256で署名します。TokenとSecretはGitやD1へ置かず、Cloudflareのsecretとして管理します。

5. 睡眠・体重: Withings Health Data API

実装済み。 Sleep Analyzerの睡眠概要とBody体重計の体重を、本人のOAuth認可後にアプリ起動時にon-demand同期します(worker/withings.ts)。同期はカーソル方式で毎回7日オーバーラップ、初回のみ過去365日を取得します。
段階URL用途
本人認可https://account.withings.com/oauth2_user/authorize2認可コードを取得。stateを検証
トークン取得・更新POST https://wbsapi.withings.net/v2/oauth2action=requesttokenでaccess/refresh tokenを取得・更新
睡眠概要POST https://wbsapi.withings.net/v2/sleepaction=getsummary。睡眠時間と取得可能な睡眠段階
体重POST https://wbsapi.withings.net/measureaction=getmeas。体重を優先

予定スコープは睡眠用user.activityと体重用user.metricsです。access tokenは短命で、refresh時には新しいrefresh tokenも返るため、成功時にD1の単一行を必ず更新します。client secretはCloudflare secretに置きます。

6. AI解説: Claude / OpenAI

実装済み(Phase D)。 POST /api/ai/explainで、Workerが計算した相関統計を外部AIに日本語で解説させます。プロバイダー(Claude / OpenAI)とモデルは実行のたびに本人が選び、選択していないプロバイダーへは送りません。自動フォールバックもありません。
Cloudflare AI Gatewayは使っていない。 当初はAI Gateway中継を計画していましたが、実装はAnthropic / OpenAIのAPIをWorkerから直接呼び出す方式に落ち着きました(他の外部API連携と同じ素のfetch方式で、バンドルを小さく保つため)。旧版に載っていたapi.cloudflare.com/.../ai/v1/*やBYOK・cf-aig-collect-log-payloadの記述は無効です。

AIへ渡す前にWorkerが行う解析

  1. 症状・気象・室内環境・睡眠・体重を、地点タイムゾーンの現地暦日で日次集約する(同じ日の複数記録は1点にまとめ、記録回数の偏りを避ける)。
  2. 症状の各部位(0〜10の平均)と各指標の間でPearson/Spearman相関・p値・有効ペア数nを算出する。
  3. 症状側には部位別に加えて「頭痛(最大)」(頭部5部位のその日のピーク)を、指標側には「起床からの経過時間(h)」を含める。後者だけは日次平均せず記録1件単位(Withings sleep_end基準、20時間超は同期漏れノイズとして除外)。
  4. n≥3のペアがある相関だけを抽出し、{ analyzed_days, correlations:[{symptom_label, metric_label, n, pearson_r, p_value, spearman_r}] }という相関統計だけを組み立てる。
  5. これをAIへ送信する。生データ・個別記録・メモ・位置情報は含めない。
選択肢実際に呼ぶURLAPI形式・既定モデル
ClaudePOST https://api.anthropic.com/v1/messagesAnthropic Messages(anthropic-version: 2023-06-01)/既定 claude-sonnet-5
OpenAIPOST https://api.openai.com/v1/chat/completionsOpenAI Chat Completions(max_completion_tokens)/既定 gpt-5.4

モデルはshared/ai-models.tsの許可リストで検証し、リスト外のモデルは400で拒否します(ブラウザが任意のモデルを呼ばせられないように)。疎通確認用のPOST /api/ai/testは小さな"ping"を送って鍵が生きているかだけを返します。

秘密情報。 Anthropic/OpenAIのキーはWorker SecretANTHROPIC_API_KEY / OPENAI_API_KEY)にのみ保持し、ブラウザには一切露出しません。設定画面には「設定済み/未設定」と接続テスト結果だけを表示します。

AIへ送るもの・送らないもの

送る送らない
解析対象日数、相関ごとのn(有効ペア数)時間別の気象・室内環境の生データ
症状ラベル・指標ラベルと相関係数(Pearson/Spearman)、p値個別の痛み記録、記録時刻、メモ
Workerが決定論的に算出した相関統計のみ緯度・経度、地点名、OAuthトークン、APIキー

AIの役割は数値計算ではなく、傾向・不確実性・軽い生活上の提案を説明することです。診断・薬の変更・治療指示は行わせず、画面にも医療診断ではない旨を固定表示します。

7. 実装状況と運用上の再確認点

段階内容状態運用で再確認すること
BSwitchBot室温・室湿度(毎時Cron蓄積)実装済み所有機種のstatusレスポンス項目、Cloud Service有効化
CWithings睡眠・体重(OAuth・7日オーバーラップ)実装済み登録アプリのredirect URI、scope、token更新
統合全データ源の日次解析・CSV実装済み欠損と複数回記録の集約規則
DAI解説(相関統計のみ送信・モデル選択・接続テスト)実装済み既定モデル名(claude-sonnet-5 / gpt-5.4)、許可リスト、Worker Secret

外部APIは変更され得る。ここに記載したURL・認証・モデル仕様は2026-07-04時点のhealth-moniterソース(frontend/worker/)とdocs/design.mdに基づく。実装変更時はこのノートも更新する。

参考資料

← health-moniter 目次へ更新履歴