[{"data":1,"prerenderedAt":172},["ShallowReactive",2],{"content:\u002Ffiddling\u002Fheart-rate-to-grafana":3,"surround:\u002Ffiddling\u002Fheart-rate-to-grafana":161},{"id":4,"title":5,"body":6,"categories":136,"date":138,"description":139,"draft":140,"extension":141,"image":142,"meta":143,"navigation":145,"path":146,"permalink":142,"published":142,"readingTime":147,"recommend":142,"references":142,"seo":152,"sitemap":153,"stem":154,"tags":155,"type":159,"__hash__":160},"content\u002Fposts\u002Ffiddling\u002Fheart-rate-to-grafana.md","My heart beats for U —— 心率同步 Grafana 展示",{"type":7,"value":8,"toc":133},"minimark",[9,13,20,27,38,44,47,83,86,89,98,107,110,118,121,124,130],[10,11,12],"p",{},"整了个小活：把苹果健康的心率定时同步到服务器上，并由 Grafana 绘制展示，效果大概如下：",[10,14,15],{},[16,17],"img",{"alt":18,"src":19},"","https:\u002F\u002Fblog-img.774352199.xyz\u002F2025\u002Fe01807e95f9c8ea4384d2c4d8f4fe3cb.png",[10,21,22,26],{},[23,24,25],"del",{},"可以点击博客右上角的 ♥️ 标志查看，因为套了 Cloudflare Tunnel，国内访问速度不佳，尽量挂🪜访问。","已下线，由于更换了 Oppo 手机，心率无法同步上传了",[10,28,29,30,37],{},"大概的思路就是使用这个 app ",[31,32,36],"a",{"href":33,"rel":34},"https:\u002F\u002Fapps.apple.com\u002Fus\u002Fapp\u002Fhealth-auto-export-json-csv\u002Fid1115567069?l=zh-Hans-CN",[35],"nofollow","Health Auto Export - JSON+CSV"," 的 Restful API 的功能，设置定时将心率数据发送到部署的 http 接口以写入 InfluxDB，再由 Grafana 连接 InfluxDB 绘制看板",[10,39,40,43],{},[31,41,36],{"href":33,"rel":42},[35]," 这个 app 如果想要定时同步，需要开通 Premium，Lifetime 美区是 24.99 USD，有点小贵，但似乎也没有什么平替方案",[10,45,46],{},"订阅后新建一个 Automation：",[48,49,50,59,65,71,77,80],"ul",{},[51,52,53,54,58],"li",{},"Automation Type 为 ",[55,56,57],"code",{"code":57},"REST API","​",[51,60,61,62,58],{},"URL 就是你下面要部署的服务的地址，API 路径为 ",[55,63,64],{"code":64},"\u002Fpush\u002Fheart_rate",[51,66,67,68,58],{},"Data Type 选择 ",[55,69,70],{"code":70},"Health Metrics",[51,72,73,74,58],{},"Select Health Metrics 勾选 ",[55,75,76],{"code":76},"Heart Rate",[51,78,79],{},"Export Format 选择 JSON",[51,81,82],{},"Sync Cadence 可以选择 1 分钟也可以选择 5 分钟，Apple Watch 并不会一直监测心率",[10,84,85],{},"勾上 Enable 即可，为了保证在程序退出后还能同步，可以添加一个桌面小组件",[10,87,88],{},"接着就是部署服务开放 Restful API 端口，接收数据并写入 InfluxDB 了。部署 InfluxDB 可以自行 Google 不再赘述，注意服务用的是 InfluxDB 2",[10,90,91,92,97],{},"服务的源码在 ",[31,93,96],{"href":94,"rel":95},"https:\u002F\u002Fgithub.com\u002Freekystive\u002Fhealthkit-collector",[35],"reekystive\u002Fhealthkit-collector","，是个 node 项目，可以直接用 pnpm 启动监听 3000 端口。我写了个 Dockerfile 将其打包成 docker 镜像，部署在家里的服务器上",[99,100,105],"pre",{"className":101,"code":103,"language":104,"meta":18},[102],"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",[55,106,103],{"__ignoreMap":18},[10,108,109],{},"启动时需要设置四个连接 InfluxDB 使用的环境变量",[99,111,116],{"className":112,"code":114,"language":115},[113],"language-text","INFLUXDB_TOKEN='your_influxdb_token'\nINFLUXDB_URL='your_influxdb_url'\nINFLUXDB_ORG='your_influxdb_org'\nINFLUXDB_BUCKET='your_influxdb_bucket'\n","text",[55,117,114],{"__ignoreMap":18},[10,119,120],{},"部署完成后可以尝试进行一次同步，服务会输出写入 DB 成功的 log",[10,122,123],{},"最后就是部署个 Grafana 绘制仪表盘了，添加好 Data Source，新建仪表盘，查询语句如下",[99,125,128],{"className":126,"code":127,"language":115},[113],"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",[55,129,127],{"__ignoreMap":18},[10,131,132],{},"Enjoy！",{"title":18,"searchDepth":134,"depth":134,"links":135},4,[],[137],"折腾","2025-03-31 23:51:00","通过将苹果健康的心率数据定时同步到服务器，使用 Grafana 进行可视化展示，创造了一种直观的健康监测方式。利用 Health Auto Export 应用的 Restful API，将心率信息发送到指定的 http 接口，存储在 InfluxDB 中，最终在 Grafana 中呈现出清晰的看板，便于追踪和分析个人的心率变化。",false,"md",null,{"slots":144},{},true,"\u002Ffiddling\u002Fheart-rate-to-grafana",{"text":148,"minutes":149,"time":150,"words":151},"4 min read",3.2,192000,640,{"title":5,"description":139},{"loc":146},"posts\u002Ffiddling\u002Fheart-rate-to-grafana",[137,156,157,158],"Grafana","心率","Apple Watch","tech","Cza4iIJX4OO-pAWu8mloqDfq5E0S1y-xs1M_kJVxCIE",[162,167],{"title":163,"path":164,"stem":165,"date":166,"type":159,"children":-1},"语法分析中类型名-变量名歧义消除","\u002Ffiddling\u002Fparser-type-variable-ambiguity","posts\u002Ffiddling\u002Fparser-type-variable-ambiguity","2025-03-15 20:35:00",{"title":168,"path":169,"stem":170,"date":171,"type":159,"children":-1},"上海杭州异地组网记录","\u002Ffiddling\u002Fcross-city-network-setup","posts\u002Ffiddling\u002Fcross-city-network-setup","2025-04-18 16:43:12",1787554444234]