双设备健康数据管道:从分散到统一的自动化采集方案
如何将两个穿戴设备(Whoop + Garmin)的健康数据统一采集、结构化存储,并用于跨设备交叉分析。
问题
用户同时佩戴 Whoop(恢复/睡眠)和 Garmin(训练/GPS),两个设备各有擅长但数据完全隔离。
每天需要回答的核心问题是”今天该练什么”,但答案藏在两个设备的数据交叉点上——单看任何一个 App 都不够。
根因:设备厂商不开放数据互通,用户只能在各自 App 里看各自的图表。
核心概念
双设备互补模型
whoop:
擅长: 恢复评估、睡眠质量、自主神经状态
核心指标:
- 恢复分 (0-100, ≥67 为绿区)
- HRV RMSSD (自主神经状态)
- 睡眠表现 (实际 vs 目标)
- 睡眠债 (累计不足)
API: 官方开发者 API,标准 OAuth2,文档清晰
garmin:
擅长: 训练负荷管理、GPS 轨迹、运动表现
核心指标:
- 训练状态 (Productive/Maintaining/Detraining/Overreaching)
- ACWR 急慢比 (甜区 0.8-1.3)
- 压力分 (全天压力水平)
- VO2Max 趋势
API: 无官方个人开发者 API,依赖第三方库模拟移动端认证
关键差异:Whoop 告诉你”身体准备好了吗”,Garmin 告诉你”最近练够了吗”。两者组合才能做出合理的训练决策。
数据格式:人机双读快照
每日快照采用 Markdown + 隐藏 JSON 的混合格式:
格式设计原则:
人类层: Markdown 正文,直接阅读
机器层: HTML 注释内嵌 JSON,正则提取
存储位置: GitHub 仓库,按设备/日期组织
版本管理: git log 追溯历史变化
为什么不用纯 JSON:纯 JSON 在 GitHub 上不可读。纯 Markdown 又不方便程序解析。HTML 注释是两全的方案——渲染时不可见,解析时一行正则搞定。
方法:跨设备训练决策
决策矩阵
| Whoop 恢复分 | Garmin ACWR | 判断 | 建议 |
|---|---|---|---|
| ≥67 (绿区) | < 0.8 | 恢复好 + 练少了 | 上强度,增加训练量 |
| ≥67 (绿区) | 0.8-1.3 | 状态健康 | 按计划执行 |
| ≥67 (绿区) | > 1.3 | 恢复好但负荷偏高 | 按计划但关注疲劳信号 |
| 34-66 (黄区) | 任意 | 恢复中等 | 降强度,有氧为主 |
| < 34 (红区) | 任意 | 疲劳/过度训练 | 休息或轻度恢复跑 |
预警规则
趋势预警:
- condition: HRV 连续 3 天下降 > 10%
signal: 可能过度训练
action: 建议减量 1-2 天
- condition: 睡眠债累计 > 5 小时 (滚动 7 天)
signal: 睡眠不足积累
action: 优先补觉,降低训练强度
- condition: ACWR > 1.5
signal: 急性负荷过高
action: 伤病风险升高,强制减量
- condition: 恢复分连续 3 天红区
signal: 系统性疲劳
action: 完全休息,排查压力源
架构:数据管道
数据流
实时路径:
Whoop 手环 → Whoop Cloud → Webhook 推送 → 处理脚本 → 微信通知 + 落盘
定时路径 (每天 21:00):
Whoop API → 拉取当天数据 → 每日快照
Garmin Connect → 第三方库拉取 → 每日快照
合并两设备简报 → 微信推送
快照文件 → GitHub 同步
周报路径 (每周一 08:30):
读取近 7 天快照 → 生成趋势汇总表 → 微信推送
调度策略
为什么用 Agent 级调度而非系统 crontab:
- Token 过期时 Agent 可自主刷新重试
- API 路径变化时 Agent 可自主切换
- 失败时 Agent 可判断原因并决定是重试还是告警
- 不需要人类半夜起来改脚本
已验证的踩坑经验
Garmin 认证
坑1_全局限流:
现象: garth.login() 返回 429
原因: Cloudflare 全局级限流,不分 IP
解法: 停止所有重试,等 1-2 小时自然冷却
教训: 每次重试都会重置冷却窗口
坑2_认证体系不互通:
现象: 浏览器能登录 Garmin Connect,但 cookie 无法转成 API token
原因: Web SSO (CAS) 和移动端 OAuth 是两套独立体系
解法: 用旧版 Web SSO 拿 CAS ticket → 用库底层方法 exchange 为 OAuth token
关键: ticket 有效期只有几秒,必须立即交换
坑3_API路径变化:
现象: 之前能用的 URL 突然返回 403
原因: 带用户 GUID 的路径被禁,不带的正常
解法: 使用库的高级封装方法,不手动拼 URL
教训: 非官方 API 随时可能变,没有 changelog
Token 管理
Whoop:
- access_token 可被服务端提前吊销(即使本地未过期)
- 遇到 401 必须强制刷新而非信任本地过期时间
- created_at 时区是 UTC,文档未说明
Garmin:
- refresh_token 约 30 天有效
- 正常使用走 refresh_oauth2(),不触发登录限流
- 只有 refresh_token 也过期时才需要重新走 SSO 流程
适用场景
这套方案适用于任何需要跨设备健康数据整合的场景:
- 马拉松训练决策(本文场景)
- 慢性疲劳监控
- 睡眠优化实验(改变习惯 → 观测数据变化)
- 运动处方调整(配合教练使用)
核心思路是通用的:把分散在各个 App 里的数据拿回来,用自己的逻辑分析。