Oura Ring 集成
连接用户的 Oura Ring,通过 Oura Cloud API V2 获取睡眠、心率、准备状态、活动及其他健康数据。
前置条件
- 一个 Oura Ring(第3代或 Ring 4),并拥有有效的 Oura 会员资格(API 访问需要每月 $6)
- 已安装 Oura 应用并与戒指配对
- 在 https://cloud.ouraring.com 拥有 Oura Cloud 账户
设置
1. 注册开发者应用
让用户访问 https://developer.ouraring.com/applications 并创建一个新应用:
- 显示名称: 用户希望用于集成的名称
- 描述: 集成的简要描述
- 联系邮箱: 用户的邮箱
- 网站: 任何有效的 URL(例如个人网站)
- 隐私政策 / 服务条款: 任何有效的 URL(必填字段,但对个人应用不作强制)
- 重定向 URI:
http://localhost:3000/callback - 作用域: 启用所有需要的作用域。推荐:
personal,daily,heartrate,sleep,workout,spo2,stress,heart_health,session,ring_configuration
保存客户端 ID 和客户端密钥。
2. 运行 OAuth 流程
使用 assistant credentials prompt 安全地存储客户端密钥,然后在用户的机器上编写并运行 OAuth 辅助脚本:
#!/usr/bin/env python3
"""Oura Ring OAuth2 辅助程序 — 即时捕获授权码并交换为令牌。"""
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs, urlencode
import urllib.request, json, webbrowser, ssl
CLIENT_ID = 'YOUR_CLIENT_ID'
CLIENT_SECRET = 'YOUR_CLIENT_SECRET'
REDIRECT_URI = 'http://localhost:3000/callback'
SCOPES = 'email personal daily heartrate workout tag session spo2 ring_configuration stress heart_health'
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path == '/':
params = urlencode({
'response_type': 'code', 'client_id': CLIENT_ID,
'redirect_uri': REDIRECT_URI, 'scope': SCOPES, 'state': 'assistant'
})
self.send_response(302)
self.send_header('Location', f'https://cloud.ouraring.com/oauth/authorize?{params}')
self.end_headers()
elif parsed.path == '/callback':
code = parse_qs(parsed.query).get('code', [''])[0]
if code:
token_data = urlencode({
'grant_type': 'authorization_code', 'code': code,
'redirect_uri': REDIRECT_URI, 'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
}).encode()
try:
req = urllib.request.Request('https://api.ouraring.com/oauth/token',
data=token_data,
headers={'Content-Type': 'application/x-www-form-urlencoded'},
method='POST')
resp = urllib.request.urlopen(req, context=ssl.create_default_context())
result = json.loads(resp.read())
with open('/tmp/oura_tokens.json', 'w') as f:
json.dump(result, f, indent=2)
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.end_headers()
self.wfile.write(b'<html><body><h1>已连接!您可以关闭此标签页。</h1></body></html>')
print(f'\n令牌已保存到 /tmp/oura_tokens.json')
except Exception as e:
self.send_response(500)
self.end_headers()
self.wfile.write(f'令牌交换失败: {e}'.encode())
def log_message(self, *a): pass
print('正在打开浏览器进行 Oura 授权...')
webbrowser.open('http://localhost:3000')
HTTPServer(('localhost', 3000), Handler).serve_forever()
在用户的机器上使用 python3 /path/to/script.py 运行(host_bash)。用户在浏览器中授权,脚本会捕获授权码并在不到一秒内交换为令牌。
重要: 授权码约在 30 秒内过期。不要让用户手动粘贴授权码——使用此脚本自动捕获并交换。
3. 存储令牌
OAuth 流程完成后,从 /tmp/oura_tokens.json 读取令牌并存储:
access_token— 存储在凭据库中,注入模板用于api.ouraring.com的 Authorization 头(Bearer 前缀)和allowed_tools: ["bash"]refresh_token— 存储在凭据库中,用于令牌刷新- 令牌约在 30 天后过期。设置提醒在过期前刷新。
4. 验证连接
使用个人信息端点进行测试:
curl -s -H "Authorization: Bearer $TOKEN" "https://api.ouraring.com/v2/usercollection/personal_info"
可用端点
所有端点均使用 GET https://api.ouraring.com/v2/usercollection/{type},查询参数为 start_date 和 end_date(YYYY-MM-DD 格式)。
| 端点 | 数据 | 说明 |
|---|---|---|
/v2/usercollection/daily_sleep | 睡眠评分、时长、效率、阶段 | 最好在用户通常的醒来时间后检查 |
/v2/usercollection/sleep | 详细的睡眠时段,包括心率、HRV、运动 | 原始睡眠时段数据 |
/v2/usercollection/daily_readiness | 准备度评分、HRV 平衡、恢复 | 早晨检查指标 |
/v2/usercollection/daily_activity | 步数、卡路里、运动、静止时间 | 活动摘要 |
/v2/usercollection/heartrate | 连续心率(使用 start_datetime/end_datetime ISO 格式) | 数据可能较大 — 限制日期范围 |
/v2/usercollection/daily_spo2 | 血氧水平 | 夜间平均值 |
/v2/usercollection/daily_stress | 压力评分和恢复 | 日间压力跟踪 |
/v2/usercollection/workout | 检测到的锻炼,包括心率、卡路里 | 自动检测或手动记录 |
/v2/usercollection/personal_info | 年龄、体重、身高、邮箱 | 良好的连接测试 |
令牌刷新
令牌在 30 天后过期。使用以下命令刷新:
curl -s -X POST "https://api.ouraring.com/oauth/token" \
-d "grant_type=refresh_token&refresh_token=$REFRESH_TOKEN&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET"
从响应中存储新的 access_token 和 refresh_token。
提示
- 新戒指需要 1-2 晚进行校准,之后数据才有意义
- 睡眠数据通常在用户醒来几小时后出现
- 锻炼期间的心率数据在锻炼通过 Oura 应用同步后出现
- 速率限制:每 5 分钟 5000 次请求
- Ring 4 和第 3 代用户必须拥有有效的 Oura 会员资格才能使用 API