garmin-cn
by Moun-xz
README.md
# 佳明中国区训练数据 → 本地 SQLite → AI 直接分析
把佳明手表的数据变成 AI 能读的数据库,然后在 WorkBuddy / Claude 里用人话提问:「最近四周负荷是不是加太快了」「昨晚睡得怎么样」。
**中国区账号(connect.garmin.cn)可以直接用,不需要换区、不需要跨区桥接工具。**
```
手表 ──蓝牙──> 手机 Garmin Connect ──> connect.garmin.cn
│ garminconnect + is_cn=True
▼
sync.py 增量同步(按需触发)
▼
data/garmin.db (SQLite)
▼
mcp_server.py(本地 MCP 服务,9 个工具)
▼
WorkBuddy / Claude Desktop 对话
```
数据全程只在本机。密码只在首次登录用一次,之后走 token,不上传任何第三方。
---
## 一、这件事为什么值得做
佳明官方的数据出口有三个问题:
| 渠道 | 问题 |
|---|---|
| Garmin Connect 网页 | 图表浅,只能看单次,做不了趋势对比和自定义统计 |
| 官方 Health / Training API | 需要公司身份申请商业审批,个人基本拿不到 |
| 第三方平台(Strava / Runalyze / Intervals.icu) | **中国区账号拿不到授权**,绑上也读不到数据 |
而中国区账号的唯一官方客户端 `connect.garmin.cn` 又是封闭的。所以路子只剩一条:**在本机模拟网页登录,把数据拉下来自己存。**
好消息是 `garminconnect` 这个库支持中国区,只差一个参数。
---
## 二、和其它方案比
| 方案 | 中国区可用 | 历史数据 | 能否被 AI 读 | 成本 |
|---|---|---|---|---|
| 官方全量导出 FIT | ✅ | 全部 | ❌ 要自己解析 | 免费 |
| GarminDB | ✅ | 全部 | 部分 | 免费 |
| 托管 MCP(AthleteData / stats.training) | ❌ 需国际区 | 30 天 | ✅ | $9-10/月 |
| **本项目** | ✅ | 全部 | ✅ | 免费 |
本项目 ≈ `garminconnect` + SQLite + 一个 MCP 服务,一共约 1400 行 Python。
---
## 三、快速开始
### 1. 环境
```bash
python -m venv .venv
.venv/bin/pip install -r requirements.txt # Windows: .venv\Scripts\pip.exe
```
### 2. 登录(唯一需要手动的一步)
```bash
# Windows
.venv\Scripts\python.exe sync.py --login
# macOS / Linux
.venv/bin/python sync.py --login
```
输入**佳明中国区**账号(邮箱或手机号)和密码,开了双重验证会要求输验证码。
成功后 token 存在 `data/tokens/`,之后再也不会问你要密码。
> 密码只在这一次登录里使用,不落盘。如果脚本在非交互环境跑(比如被 AI 调用),会直接读 `GARMIN_EMAIL` / `GARMIN_PASSWORD` 环境变量。
### 3. 拉历史
```bash
.venv\Scripts\python.exe sync.py --full
```
活动默认一路往回翻到最早一条;健康数据每天要 5 个请求,默认只拉 30 天(想补更早的用 `--health-days 90`)。
### 4. 接到 AI
MCP 服务配置写进 `~/.workbuddy/mcp.json`(Claude Desktop 是 `claude_desktop_config.json`):
```json
{
"mcpServers": {
"garmin-cn": {
"command": "/绝对路径/到本项目/.venv/bin/python",
"args": ["/绝对路径/到本项目/mcp_server.py"],
"env": { "PYTHONUTF8": "1" }
}
}
}
```
> Windows 下把 `command` 换成 `D:/你的路径/.venv/Scripts/python.exe`,路径用正斜杠可以省掉转义。
> **写完必须完全重启客户端**(不是关窗口,是从托盘退出)。本地 MCP 不会出现在「连接器市场」里,它在设置页的 MCP Servers 区块,需要手动信任一次。
---
## 四、四个文件在干什么
### `store.py` — 数据落地
四张表,全部保留原始 JSON,方便以后解析新字段而不用重新拉数据:
| 表 | 内容 |
|---|---|
| `activities` | 活动列表:距离、时长、配速、心率、爬升、训练效应、VO2max |
| `splits` | 单次活动的分段明细(按需拉取,只拉看过的) |
| `daily` | 每日健康:静息心率、HRV、睡眠分期、Body Battery、准备度、负荷 |
| `meta` | 键值对,主要存 `last_sync` |
```python
conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA journal_mode=WAL") # 关键:同步写入时读不阻塞
```
### `sync.py` — 增量同步
三条原则:
1. **token 优先**。有 token 就直接用,绝不再问密码
2. **只补缺的**。先查库里已有的日期,只拉缺口
3. **慢一点**。每次请求间隔 0.8 秒,遇 429 指数退避
```python
client = Garmin(email, password, is_cn=True) # ← 中国区唯一的开关
client.login(str(TOKENS)) # 有 token 走这条
```
### `mcp_server.py` — 把库暴露成工具
9 个工具,AI 按需调用:
| 工具 | 作用 |
|---|---|
| `db_info` | 库状态:多少条、数据到哪天、上次同步 |
| `sync_now` | 立刻增量同步 |
| `list_activities` | 最近 N 次训练清单,可按运动类型筛 |
| `activity_detail` | 单次训练分段明细(首次调用才去拉) |
| `daily_health` | 静息心率 / HRV / 睡眠 / Body Battery / 负荷 |
| `training_summary` | **主入口**:按周汇总 + 负荷比 + 配速心率漂移 |
| `sleep_report` | 多日睡眠:明细、睡眠债、作息规律性、红旗 |
| `morning_briefing` | 早起汇报:昨晚恢复 + 今日建议 |
| `query_sql` | 只读 SQL,自定义统计 |
**设计上最有用的一个决定**:查询类工具在开始前会自动检查 `last_sync`——
- 距上次同步 < 6 小时 → 直接读本地,秒回
- 超过 6 小时 → 先增量同步一次再回答
- 同步失败 → 不报错,用旧数据回答 + 开头加一句提示
所以**不需要任何定时任务**。你什么时候问,它就什么时候保证数据是新的。
### `briefing.py` — 口语化的恢复汇报
早上想看「昨晚睡得怎么样」,最没用的做法是把一堆字段推给你。这个文件把数字翻译成人话:
```
昨晚 7 小时 30 分,比你这周的平均(6.8 小时)好一点。够数了。
00:39 躺下、08:12 起,这个入睡时间算你近期偏早的。
夜间 HRV 152,比你 28 天的基线(132)高了一截,恢复是往上走的。
今天准备度 91,是最近少见的高位。昨天没怎么动,别浪费。
```
每个维度分 4-6 档(睡眠 5 档、准备度 4 档、HRV 6 档),所以连着看一周每天说的话不一样,不是填空模板。
---
## 五、踩过的坑(这部分最有价值)
### 1. 中国区只差一个参数
```python
Garmin(email, password, is_cn=True)
```
设了 `is_cn=True`,登录域名就切到 `connect.garmin.cn`;否则一律打到国际站,中国区账号必然失败。
也可以用环境变量 `GARMIN_IS_CN=1`。
### 2. token 优先于密码,顺序反了就会卡住
第一版写成这样:
```python
email = os.getenv("GARMIN_EMAIL") or input("账号: ") # ← 先问密码
password = os.getenv("GARMIN_PASSWORD") or getpass()
has_token = tokens_exist() # ← 才看 token
```
结果明明已经登录过,脚本还是停下来等输入,在非交互环境直接挂死。
正确顺序:**先看有没有 token,有就直接 `login(token_dir)`,失败了才回退到账号密码。**
### 3. MCP Python SDK 2.x 换了 API
网上绝大多数教程写的是:
```python
from mcp.server.fastmcp import FastMCP # 2.x 里已不存在
mcp = FastMCP("garmin-cn")
```
2.x 里改成了:
```python
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("garmin-cn")
```
装之前先 `pip show mcp` 看版本,否则会对着不存在的模块调试半天。
### 4. 日志绝对不能打 stdout
MCP 走 stdio 上的 JSON-RPC,**任何一行 `print()` 都会污染协议**,客户端表现为「服务连接失败」但看不到任何报错。
```python
print(line, file=sys.stderr, flush=True) # 所有日志都走 stderr
```
### 5. Windows 下中文乱码
MCP 服务的 env 里加 `PYTHONUTF8=1`(或 `PYTHONIOENCODING=utf-8`),否则返回的中文全是问号。
### 6. SQLite 并发锁
同步正在写的时候 AI 来读,会直接 `database is locked`。开 WAL 就好:
```sql
PRAGMA journal_mode=WAL;
```
实测写入事务未提交时读照样成功。
### 7. 限流是真的
佳明对频繁登录很敏感。登录成功一次就要把 token 落盘,所有后续同步复用 token。请求之间留 0.8 秒,遇 429 指数退避。
**别把同步设成几分钟一次**,这个接口扛不住。
### 8. 健康数据 = 一天 5 个请求
活动列表一次能翻很多条,但每日健康是拼出来的(睡眠、HRV、准备度、Body Battery、训练状态各一个请求)。补 90 天 ≈ 450 次请求,大概 5-8 分钟,中间容易撞限流。所以默认只拉 30 天,按需往前补。
### 9. 睡眠时间戳是 UTC,不是本地时间
`sleepStartTimestampLocal` 这个名字极具误导性——它给你的是 **UTC 毫秒**。直接 `fromtimestamp()` 会差 8 小时,算出来的「入睡时间」完全是错的。
```python
t = dt.datetime.fromtimestamp(ms / 1000, dt.timezone.utc) # 必须指定 UTC
```
### 10. 国行字段比想象中全
网上说法是国行缺训练准备度、HRV 状态这些。实测(Forerunner 265 + 中国区)**17 个字段几乎全满**,训练准备度、急性/慢性负荷、HRV 状态都在。VO2max 只在部分活动中产生,属于正常。
所以别预设「国行数据不全」,先拉一遍看实际填充率再决定分析口径。
### 11. MCP 配置写完要完全重启
不是关窗口。Windows 上点 × 只是缩到托盘,配置根本没重读。要从托盘图标右键退出,再打开。
---
## 六、数据安全
- 密码只在首次登录时用一次,不写进任何文件
- token 存在 `data/tokens/`,`.gitignore` 已排除
- 数据库 `data/garmin.db` 含全部原始 JSON,同样已排除
- 所有请求直连 `connect.garmin.cn`,不经过任何中转服务器
`.gitignore` 的核心几条:
```
data/
.env
.venv/
__pycache__/
```
**注意**:`data/` 目录里同时有 token 和你的全部训练记录,分享 `garmin.db` 前记得先清空。
---
## 七、能问什么
信任 MCP 之后直接说人话,AI 会自己调工具取数:
```
看看我最近四周,负荷是不是加太快了
昨晚睡得怎么样
这周三的间歇跑,配速有没有掉
我 HRV 连着一周在降,是不是该减量
下个月有半马,基于我现在数据给个三周安排
我这一年哪几个月跑量掉得最厉害
```
比通用建议准的地方在于:**它看的是你的真实数据**,不是你描述的数据。
---
## 八、局限
1. **库会失效**。`garminconnect` 靠模拟网页登录,佳明一改版就可能断。社区通常几天内修好,`pip install -U garminconnect` 升级即可。数据在本地,不会丢。
2. **强度分布是估算的**。库里存的是活动平均心率,不是逐秒心率区间。要精确的 Z1-Z5 占比,得额外拉每次活动的 `hr_in_timezones`。
3. **更早的历史需要官方导出兜底**。部分接口只回补最近 30 天。
---
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues