[{"data":1,"prerenderedAt":182},["ShallowReactive",2],{"content:\u002Fja\u002Ffiddling\u002Fheart-rate-to-grafana":3,"surround:\u002Fja\u002Ffiddling\u002Fheart-rate-to-grafana":171},{"id":4,"title":5,"authorship":6,"body":7,"categories":139,"date":141,"description":142,"draft":143,"extension":144,"image":145,"meta":146,"navigation":148,"path":149,"permalink":150,"published":150,"readingTime":151,"recommend":150,"references":150,"seo":156,"seoDescription":157,"seoTitle":158,"sitemap":159,"stem":162,"tags":163,"type":169,"__hash__":170},"content_ja\u002Fposts\u002Ffiddling\u002Fheart-rate-to-grafana.md","My Heart Beats for U：心拍数をGrafanaで可視化する","human-only",{"type":8,"value":9,"toc":136},"minimark",[10,14,21,28,39,45,48,86,89,92,101,110,113,121,124,127,133],[11,12,13],"p",{},"ちょっとしたものを作りました。Appleヘルスケアの心拍数を定期的にサーバーへ同期して、Grafanaでグラフにします。こんな感じです。",[11,15,16],{},[17,18],"img",{"alt":19,"src":20},"","https:\u002F\u002Fblog-img.774352199.xyz\u002F2025\u002Fe01807e95f9c8ea4384d2c4d8f4fe3cb.png",[11,22,23,27],{},[24,25,26],"del",{},"ブログ右上の♥️から見られます。Cloudflare Tunnel経由なので中国国内からは遅く、できれば検閲回避用プロキシ🪜を使ってください。","現在は公開を終了しています。OPPOのスマートフォンへ替えたため、心拍数を同期・アップロードできなくなりました。",[11,29,30,31,38],{},"仕組みは、",[32,33,37],"a",{"href":34,"rel":35},"https:\u002F\u002Fapps.apple.com\u002Fus\u002Fapp\u002Fhealth-auto-export-json-csv\u002Fid1115567069?l=zh-Hans-CN",[36],"nofollow","Health Auto Export - JSON+CSV","のREST API機能で心拍数を定期的に自前のHTTPエンドポイントへ送り、InfluxDBに書き込むというものです。GrafanaをInfluxDBにつないでダッシュボードを描画します。",[11,40,41,44],{},[32,42,37],{"href":34,"rel":43},[36],"で定期同期するにはPremiumが必要です。米国ストアのLifetimeは24.99 USDと少し高めですが、安い代替手段は見当たりませんでした。",[11,46,47],{},"購入したらAutomationを新規作成します。",[49,50,51,60,67,73,80,83],"ul",{},[52,53,54,55,59],"li",{},"Automation Typeは",[56,57,58],"code",{"code":58},"REST API","。",[52,61,62,63,66],{},"URLはこのあとデプロイするサービスのアドレスで、APIパスは",[56,64,65],{"code":65},"\u002Fpush\u002Fheart_rate","です。",[52,68,69,70,59],{},"Data Typeは",[56,71,72],{"code":72},"Health Metrics",[52,74,75,76,79],{},"Select Health Metricsで",[56,77,78],{"code":78},"Heart Rate","にチェック。",[52,81,82],{},"Export FormatはJSON。",[52,84,85],{},"Sync Cadenceは1分でも5分でもかまいません。Apple Watchは常時心拍数を測定しているわけではありません。",[11,87,88],{},"Enableにチェックを入れればOKです。アプリを終了しても同期が続くよう、ホーム画面にウィジェットを置いておくとよいです。",[11,90,91],{},"次はREST APIを公開するサービスをデプロイし、受け取ったデータをInfluxDBに書き込みます。InfluxDBの導入方法は検索してください。ここでは省略しますが、このサービスはInfluxDB 2を使う点に注意してください。",[11,93,94,95,100],{},"ソースは",[32,96,99],{"href":97,"rel":98},"https:\u002F\u002Fgithub.com\u002Freekystive\u002Fhealthkit-collector",[36],"reekystive\u002Fhealthkit-collector","にあります。Nodeのプロジェクトで、pnpmから直接起動すると3000番ポートで待ち受けます。私はDockerfileを書いてDockerイメージにまとめ、自宅サーバーへデプロイしました。",[102,103,108],"pre",{"className":104,"code":106,"language":107,"meta":19},[105],"language-go","FROM node:20-alpine AS builder\n\n# Install pnpm\nRUN corepack enable && corepack prepare pnpm@9.14.2 --activate\n\n# Set working directory\nWORKDIR \u002Fapp\n\n# Copy package.json and pnpm-lock.yaml\nCOPY package.json pnpm-lock.yaml* .\u002F\n\n# Install dependencies\nRUN pnpm install --frozen-lockfile\n\n# Copy source code\nCOPY . .\n\n# Build the application\nRUN pnpm build\n\n# Stage 2: Production stage\nFROM node:20-alpine AS production\n\n# Install pnpm\nRUN corepack enable && corepack prepare pnpm@9.14.2 --activate\n\n# Set working directory\nWORKDIR \u002Fapp\n\n# Copy package.json and pnpm-lock.yaml\nCOPY package.json pnpm-lock.yaml* .\u002F\n\n# Install production dependencies only\nRUN pnpm install --prod --frozen-lockfile\n\n# Copy built application from builder stage\nCOPY --from=builder \u002Fapp\u002Fdist .\u002Fdist\n\n# Set environment variables\n# These are default values that can be overridden when running the container\nENV NODE_ENV=production\nENV PORT=3000\n\n# Expose the port your app runs on (using the PORT environment variable)\nEXPOSE ${PORT}\n\n# Command to run the application\nCMD [\"node\", \"dist\u002Findex.js\"]\n","go",[56,109,106],{"__ignoreMap":19},[11,111,112],{},"起動時には、InfluxDB接続用の環境変数を4つ設定します。",[102,114,119],{"className":115,"code":117,"language":118},[116],"language-text","INFLUXDB_TOKEN='your_influxdb_token'\nINFLUXDB_URL='your_influxdb_url'\nINFLUXDB_ORG='your_influxdb_org'\nINFLUXDB_BUCKET='your_influxdb_bucket'\n","text",[56,120,117],{"__ignoreMap":19},[11,122,123],{},"デプロイ後に同期を試すと、DBへの書き込みに成功したログが出ます。",[11,125,126],{},"最後にGrafanaを立て、Data Sourceを追加してダッシュボードを作ります。クエリは次のとおりです。",[102,128,131],{"className":129,"code":130,"language":118},[116],"from(bucket: \"bpm\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r[\"_measurement\"] == \"heart_rate\")\n  |> filter(fn: (r) => r[\"_field\"] == \"avg\" or r[\"_field\"] == \"max\" or r[\"_field\"] == \"min\")\n",[56,132,130],{"__ignoreMap":19},[11,134,135],{},"Enjoy！",{"title":19,"searchDepth":137,"depth":137,"links":138},4,[],[140],"fiddling","2025-03-31 23:51:00","Appleヘルスケアの心拍数を定期的にサーバーへ同期し、Grafanaで可視化して直感的に確認できるようにしました。Health Auto ExportのREST API機能でHTTPエンドポイントへデータを送り、InfluxDBに保存します。Grafanaのダッシュボードから、自分の心拍数の変化を追ったり分析したりできます。",false,"md","https:\u002F\u002Fblog-img.774352199.xyz\u002FF4qD2T.webp",{"slots":147},{},true,"\u002Ffiddling\u002Fheart-rate-to-grafana",null,{"text":152,"minutes":153,"time":154,"words":155},"4 min read",3.76,225600,752,{"title":5,"description":142},"Appleヘルスケアの心拍数をHealth Auto Exportから自前のAPIへ定期送信。InfluxDB 2への保存、Dockerでのサービス配置、Grafanaのグラフ作成を紹介します。","心拍数を Grafana に同期する：My heart beats for U",{"loc":149,"images":160},[161],{"loc":20},"posts\u002Ffiddling\u002Fheart-rate-to-grafana",[164,165,166,167,168],"Apple Watch","Health Auto Export","InfluxDB","Grafana","心拍数モニタリング","tech","WrkVEmTIa01caaHy6LSkfIAlhyBtHLcg372UFYEUD40",[172,177],{"title":173,"path":174,"stem":175,"date":176,"type":169,"children":-1},"構文解析で型名と変数名の曖昧さを解消する","\u002Ffiddling\u002Fparser-type-variable-ambiguity","posts\u002Ffiddling\u002Fparser-type-variable-ambiguity","2025-03-15 20:35:00",{"title":178,"path":179,"stem":180,"date":181,"type":169,"children":-1},"上海と杭州の自宅ネットワークをつなぐ","\u002Ffiddling\u002Fcross-city-network-setup","posts\u002Ffiddling\u002Fcross-city-network-setup","2025-04-18 16:43:12",1789914057819]