💍

Oura APIを使って自分の生死状態が誰でも確認できるWebアプリをつくる

2023/12/16に公開

🎄この記事は、秋葉原ロボット部 Advent Calendar 2023の16日目の記事です。

まずはできたもの

https://alive.uko.jp

👆こちら👆

なぜOura?


図引用: https://ouraring.com

スマートリングの1つである「Oura Ring」を2年ほど利用しています。
元々は、APIを使って生体情報を取得してみたいということで購入したのですが、なんだかんだでただ装着してるだけで全然開発できてなかったので、このタイミングで試してみようかな〜と思ってさくっと作ってみました。

Oura APIからデータを取得してみる

APIを使うには「Ouraメンバーシップ」というサブスクサービスに加入している必要があります。

  1. Personal Access Tokensページにアクセスしてログイン
  2. Create New Personal Access Token をクリック
  3. 入力欄が出てくるので、トークンの用途などのメモ書きを入力して作成
  4. 作成されたトークンをコピー
  5. Oura API Documentation (2.0)のAuthenticationの例に従って、Postmanに以下のように入力しHTTP GETしてみる

すんなり取得できました。

運動スコアとか心拍数などをとってみる

とれるデータはいくつかあるようなので試してみます。大半のデータは1日単位のサマリを返すものになっているので、リアルタイム性は期待しないほうがよさそうでした。

アクティビティ(日単位)

開始日と終了日を YYYY-MM-dd 形式の文字列で指定します(おそらくUTC基準)。
歩数やMET値などかなり色々な情報が入ってます。1日じゅう家でだらっとしてたりすると完全にバレてしまうやつ。リングを装着していない時間もちゃんと出てるのは便利ですね。

レディネススコア(日単位)

すっきりした日本語に訳しにくいのですが、体の準備度合い(≒体調)を0から100の値で示しているもののようです。いつもと体温がどれぐらい変化しているかやちゃんと休息がとれているかなどがわかります。

酸素飽和度(日単位)

SpO2の値が取得できます。
ちょうど12月12日は高熱つきの胃腸炎で丸一日ウンウン唸っていたので、値が96台まで落ちていることがしっかり確認できます(普段は99以上)。

睡眠各種

睡眠のエンドポイントは daily_sleep sleep があり、前者は上に示すようなスコア値での評価、後者は入眠時刻、心拍変化、などの具体的な実測値を返してくれるものとなっています。
あとは sleep_time というものもあり、これは「推奨される就寝時刻」を教えてくれるものになってるようなんですが、なぜかデータが取得できませんでした。

心拍数

開始日時と終了日時をISO8601形式(YYYY-MM-DDThh:mm:ss)で指定します。
APIドキュメントでは5分おきとのことですが、わりと不定期に取得されています。最新データは2時間前となっており、思ったよりリアルタイムにクラウドに反映されている印象です。

とりあえず心拍数で表示が切り替わるWebページを作ることにする

APIでいろいろ情報取得した結果、比較的リアルタイム性が高そうで、一般公開しても問題なさそう(?)な心拍数に基づいて表示が変わるWebページを作ってみようと思います。
サーバー内のディレクトリ構成は以下のようにしてみることにします。

  • ルートディレクトリ
    • cronで5分おきにOuraAPIを叩く.py
    • APIの結果を格納する.json
    • setIntervalで定期的にjsonを取得するWebページ.html

サーバサイドでOuraAPIを定期的に叩くPython

Oura DevelopersのドキュメントにはPythonのサンプルが置いてあるので、さらっと書き直します。現在日時から半日前までのデータを取得し、その中から最新のデータの日時と心拍数をJSON形式で data.json に保存するという挙動のPythonです。

cron.py
import requests, json, datetime

# 現在日時と12時間前の日時のDateオブジェクト作成
dt_now = datetime.datetime.now()
td_12h = datetime.timedelta(hours=12)
dt_12h_ago = dt_now - td_12h

# パラメータ
url = 'https://api.ouraring.com/v2/usercollection/heartrate'
params={
    'start_datetime': dt_12h_ago.isoformat(),
    'end_datetime': dt_now.isoformat()
    }
headers = {'Authorization': 'Bearer <トークン文字列>'}

# API叩く
response = requests.request('GET', url, headers=headers, params=params)
data = json.loads(response.text)
hrs = data['data']

# 要素があればたぶん生きてる(無ければ12時間以上データ未取得)
if len(hrs) > 0:
    latest = hrs[-1]
    del latest['source'] # 状態は消しておく
    with open('data.json', mode='w', encoding='utf-8') as f:
        json.dump(latest, f)
else:
    with open('data.json', mode='w', encoding='utf-8') as f:
        json.dump({}, f)

サーバにあげておき、cronで5分おきに1回動作するよう設定します。
pyenv を使っているのでPythonの実体位置を指定して実行するようにします。

% crontab -l
*/5 * * * * $HOME/.pyenv/shims/python /てきとうなディレクトリ/cron.py

Pythonスクリプトに直接アクセスが行かないよう保護も忘れずに。nginxでの例です。

nginx.conf
+ location ~ \.py$ {
+   deny all;
+ }

JSON内容で見た目が変わるWebページ

CanvaのAIによる画像生成を使い、ニワトリのイラストをいくつか作ってみました。

元気度の異なるニワトリ3種類と、死んでるかもしれない場合に表示されるローストチキンです。
心拍数の度合いに応じてこれらを切り替えられるWebページを作ります。

Vue3を使う

https://ja.vuejs.org/guide/quick-start

TypeScriptでさくっと作りたいので pnpm create vue@latest で開始します。

Google Fonts対応

https://qiita.com/to3izo/items/5dae9678c34aeaaa1936

記事執筆時点でdeprecatedになっていたので以下を使います。

https://github.com/cssninjaStudio/unplugin-fonts

vite.config.ts
+ import Unfonts from 'unplugin-fonts/vite'
+ export default defineConfig({
+   plugins: [
+     Unfonts({
+       google: {
+         preconnect: false,
+         families: [ 'Kaisei Opti' ]
+       }
+     }),
+   ]
+ })

App.vueをぱぱーっと書く

シンプルなWebなのでささっと1枚にまとめて書いてしまいます。
同じディレクトリに data.json と、 0.png から 3.png までの4種類のニワトリ画像があることが前提です。

App.vue
<script setup lang="ts">
import { ref } from 'vue'

const bpm = ref(0)
const datetime = ref('')
const picture = ref('/0.png')
const subject = ref('おまちください...')
const detail = ref('')

const update = () => {
  fetch('/data.json').then(response => response.json()).then(data => {
    if ('bpm' in data && 'timestamp' in data) {
      // JSONにデータがあるとき
      bpm.value = data.bpm
      document.title = bpm.value + ' bpm'
      const d = new Date(data.timestamp)
      datetime.value = `💍 ${d.getMonth()+1}${d.getDate()}${d.getHours()}${d.getMinutes()}${d.getSeconds()}秒 取得`
      const now = new Date()
      const diff = Math.floor((now.getTime() - d.getTime()) / (60 * 1000))
      const ago = (diff > 60) ? Math.floor(diff / 60) + '時間前' : diff + '分前'
      datetime.value += ` (およそ${ago})`
      
      // 心拍数によって説明と写真をわける
      if (bpm.value >= 90) {
        picture.value = '/3.png'
        subject.value = '心拍高いとき'
        detail.value = 'いきてる'
      } else if (bpm.value >= 70) {
        // 省略...
      } else {
        picture.value = '/0.png'
        subject.value = '心拍低いとき'
        detail.value = 'しんでる'
      }
    } else {
      // 空データだったとき(12時間以内のデータが無い場合)
      // 省略...
    }
  })
}

// 最初に実行したあとは5分おきに
update()
setInterval(update, 60000 * 5)
</script>

<template>
  <div class="container">
    <main>
      <h2>{{ subject }}</h2>
      <img alt="チキン" :src=picture />
      <h1>❤️ {{ bpm }} bpm</h1>
      <p v-if="datetime != ''">{{ datetime }}</p>
      <h3 v-html="detail"></h3>
    </main>
  </div>
</template>

<style scoped>
.container {
  font-family: 'Kaisei Opti', sans-serif;
  font-weight: bold;
  text-align: center;
}
p { color: grey; }
h3 { margin-top: 1rem; }
main {
  width: 100%;
  display: flex;
  justify-content: center;
  flex-direction: column;
}
img {
  display: block;
  max-width: 400px;
  width: 100%;
  height: auto;
  margin: 1rem auto;
}
</style>

その他、OGP追加・CSS微調整・フッター追加などの小細工は割愛します。

絵文字ファビコン

https://zenn.dev/catnose99/articles/3d2f439e8ed161

Safariは非対応とのことですが、面倒なのでこれを index.html に書いて済ませてしまいます。
ダブルクオーテーションのエンコード %22pnpm run build でひっかかるので、ビルド時にはコメントアウトしておき、出力されたファイルでコメントを外しておけば大丈夫です。

できた

心拍数ぐらい公開しても大丈夫かなと思いましたが細かく記録してけばこれだけで行動予測できなくもなさそうな気がする。。。
みなさんは個人情報の取り扱いにはお気をつけください。

Discussion