Oura integration

I have an Oura ring I’ve been using for quite a while now. The thing I pay most attention to is the temperature data, actually, because it gives me a window into my hormonal cycles, but it was also useful when I was working through some sleep problems. And it’s the least offensive step-tracker ever. In fact, it’s like, past “inoffensive” and into “maybe actively nice” to wear.

I haven’t paid much attention to the recovery and readiness metrics. It’s kind of like, uh, I mean, it matches what I already know? Like “I didn’t get enough sleep and Oura says my readiness is 67.” I don’t know. I can tell when I’m in garbage physical state.

Anyway, I set up a Beeminder goal this year at something like a 10% increase over last year’s average daily step count. That’s a rate of 9320 steps per day. It’s been mostly quite easy to stay ahead of, and I now have an autoratchet of 2 days on it, so it periodically trims off extra safety buffer. I’m looking forward to seeing what my actual average over the year winds up being!

Ok, but on to the tech. So Oura has a nice api and in addition to an Oauth2 implementation for people who want to build a client app (perhaps in Beeminder’s future?), they have a nice interface for generating personal auth tokens. You can generate multiple ones, name them, and revoke them individually, so that’s pretty nice if you want to fool around with your own scripts, but also if someone else doesn’t want to deal with OAuth2, but you want to use their stuff, you can make a PAT specific for that use, and then you’ve got control to revoke just that one token without messing up your other scripts or whatever.

Ok, so it’s a nice and simple api. It was pretty straightforward grabbing steps and syncing them with Beeminder.

#! /usr/bin/ruby
# A script to fetch my steps from Oura and update my beeminder step goal
# If run with no arguments it fetches the last 5 days from Oura, and updates
# beeminder datapoints.

require 'httparty'

OPAT = "REDACTED"
OURL= "https://api.ouraring.com/v1/"
BPAT = "REDACTED"
BURL= "https://www.beeminder.com/api/v1/"
SID = 24*60*60

stepurl = BURL+"users/b/goals/step2021"
now = Time.now
params = {
  start: (now - SID*5).strftime('%F'), 
  end: now.strftime('%F')
} 

headers = {"Authorization": "Bearer #{OPAT}"}
activity = HTTParty.get(OURL+"activity", headers: headers, query: params).parsed_response["activity"]
# sleep = HTTParty.get(OURL+"sleep", headers: headers, query: params).parsed_response["sleep"]

# transform the data into date,value pairs
activity.map!{|act| 
  [Date.parse(act["summary_date"]), act["steps"]]
}

data = HTTParty.get(stepurl+"/datapoints.json", query: {auth_token: BPAT, count: 6, sort: "daystamp"}).parsed_response

# check if Beeminder has an existing datapoint before adding our data
activity.each {|date,steps|
  datapt = data.select{|dat| dat["daystamp"] == date.strftime('%Y%m%d')}.first
  if datapt == nil
    # add a datapoint
    resp = HTTParty.post(stepurl + "/datapoints.json", body: {
      auth_token: BPAT, 
      value: steps, 
      daystamp: date.strftime('%Y%m%d'), 
      comment: "From Oura. Entered at #{Time.now.iso8601}"
    })
  elsif datapt && datapt["value"] < steps
    # ok, but there is a datapoint and it has less steps than oura 
    resp = HTTParty.put(stepurl + "/datapoints/#{datapt["id"]}.json", body: {
      auth_token: BPAT,
      value: steps,
      daystamp: date.strftime('%Y%m%d'),
      comment: "#{datapt["comment"]}; Up:#{Time.now.strftime('%H:%M')}"
    })
  end
}

The hard part about this actually turned out to be getting my mac (running Big Sur) to run the dang thing in a cron job.

First I had to add cron to the ‘Full Disk Access’ permission in the Privacy settings of ‘Security & Privacy’ in System Preferences. Otherwise cron can’t actually run programs on a Mac any longer? It seems like their attempts to keep me safe from myself are making my mac less appealing as a tool for programming… anyway.

The second problem with running this script from cron was related to my ruby environment and the gems that were available etc. I have rbenv installed controlling my ruby version, but when cron runs it doesn’t load my .rc and so it was executing the script using the system ruby which didn’t have my httparty gem installed. So this is what my cron entry wound up looking like in order to run the script:

50 * * * * /bin/zsh -c 'export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH"; ruby $HOME/bin/fetchstepsoura.rb'

6 Likes

Hmmmm. One of my friends (who does triathalons) is a big fan of her Oura; hearing that it has a pleasant API is a huge plus in its favor. Once my fitbit bites the dust, I might head Oura-wards. Bookmarking this thread for if that ever happens! :slight_smile:

1 Like

@bee Nice write-up! It seems that since 10 days ago, it is now required for laggards to get a $6 a month membership to use the app[0]. It is not clear to me if the ring is usable without the app. I.e. would we be able to get the data out using the API?

[0] https://support.ouraring.com/hc/en-us/articles/4409231414163-Upgrading-from-Gen2-to-Gen3

1 Like

If you already own an Oura Ring, you do not need the membership to use the app.

From the FAQs:

Can I continue to use my Generation 2 Oura Ring without an Oura Membership?

Yes. If you’re currently a Gen2 Oura Ring member and decide to stick with your Gen2 ring without upgrading to the new Gen3 ring, we’ll continue to support your product with routine software and firmware updates for the foreseeable future. You will not be charged a monthly membership fee. Please note that Oura Membership only applies to the Gen3 ring.

You’ll still have access to all the same features currently available on your Gen2 ring with the exception of one change to our Moment feature. You’ll still have access to unguided Moment sessions, but due to technical limitations, guided audio Moment sessions will be removed from the Gen2 experience. All existing guided audio sessions will be available in the new Explore tab for Gen3 users. Due to the technological differences between Gen2 and Gen3, as we continue to release new features, many of them may not be available on Gen2 due to limitations in its hardware components.

And given this wording, I assume the API will still be accessible:

You’ll still have access to all the same features currently available on your Gen2 ring

1 Like

Hey. I have an Oura ring and I want to use it with Beeminder to force myself to go to sleep by 01:00am.

That is if Oura time in bed start time > 01:00am then it derails me on Beeminder.

Oura would only supply the data the day after. It’s not real time but that doesn’t matter if I am considered derailed the next day.

I don’t have the knowledge and time to write a script thigh I am technical and understand that should be quite easy to simply get from Oura the start time of bedtime.

I am walling to pay if someone can help me out here. Not sure how to set up the BM goal either.

I keep going to bed late.

2 Likes

@spider I’m caught up in other side projects for the next half year or so but after that I’d be happy to do that for you for free and make it open source so others here can use it too, if anyone is still interested at that point. (If you need it sooner, which is understandable, Fiverr is a good place to find people to do small projects like this for a low price).

Side question: I was considering doing the same thing, but realized that some days I’m just not tired at 1am so I’d be sitting in bed doing nothing for an hour. What would you do if you just aren’t tired at 1am?

1 Like

Thanks @aramb.

First, I would setup an app that blocks access to apps on the phone so that I wouldn’t have anything interesting to do before bed or when already in bed.

Second, by the nature of building the habit to be in bed by say 1am, your day would be designed better so you are tired around 1am.

Another way is to use this sleeping goal on-demand, and what I mean by that is that I would probably know by afternoon, depending on how early I woke up and what I have planned for the day, so I would set up the goal with a deadline of tomorrow.

A bit more involved as BM wasn’t built for a goal with a deadline of tomorrow but if I need to mess with BM for 3 minutes to set up the goal in the evening and it saves me 2 hours of procrastinating going to bed, then I’m all for it.

1 Like

Thanks, @bee!

Here is the same thing for “bedtime before midnight in hours” in case anyone is interested.

Example:

Oura bedtime value sent to beeminder
23:30 0.5
03:00 -3.0

I use a “Do More” goal with a rate of 0.5 because on average, I want to go to bed at 23:30.

Datapoints are not updated, since bedtime doesn’t change.

I reused all your code because

  1. it works and
  2. I have never written anything in Ruby before, so I would totally mess up the logic.

:pray::pray::pray:

#! /usr/bin/ruby
# Original script: bee from beeminder --> https://forum.beeminder.com/t/oura-integration/9592
# A script to fetch my bedtime from Oura and update my beeminder goal.
# If run with no arguments it fetches the last 5 days from Oura. Does not update existing datapoints.

require 'httparty'

OPAT = "YOUR_OURA_TOKEN"
OURL= "https://api.ouraring.com/v1/"
BPAT = "YOUR_BEEMINDER_TOKEN"
BUSR = "YOUR_BEEMINDER_USERNAME"
BURL = "https://www.beeminder.com/api/v1/"
GOAL = "YOUR_BEEMINDER_GOAL_NAME"
SID = 24*60*60

stepurl = BURL+"users/"+BUSR+"/goals/"+GOAL
now = Time.now
params = {
  start: (now - SID*5).strftime('%F'), 
  end: now.strftime('%F')
} 

headers = {"Authorization": "Bearer #{OPAT}"}
# activity = HTTParty.get(OURL+"activity", headers: headers, query: params).parsed_response["activity"]
sleep = HTTParty.get(OURL+"sleep", headers: headers, query: params).parsed_response["sleep"]

# transform the data into date,value pairs
sleep.map!{|act| 
  [Date.parse(act["summary_date"]), act["bedtime_start_delta"]]
}

data = HTTParty.get(stepurl+"/datapoints.json", query: {auth_token: BPAT, count: 6, sort: "daystamp"}).parsed_response

# check if Beeminder has an existing datapoint before adding our data
sleep.each {|date,bedtime_start_delta|
  datapt = data.select{|dat| dat["daystamp"] == date.strftime('%Y%m%d')}.first
  if datapt == nil
    # add a datapoint
    resp = HTTParty.post(stepurl + "/datapoints.json", body: {
      auth_token: BPAT, 
      value: -bedtime_start_delta.to_f/60/60,  # convert to hours before midnight, make float
      daystamp: date.strftime('%Y%m%d'), 
      comment: "From Oura. Entered at #{Time.now.iso8601}"
    })
  end
}

*edit 1: accidentally deleted and recovered
*edit 2: adding a reply to spider

@spider, the script should get you the bedtime. Remove the minus before -bedtime_start_delta.to_f/60/60 if you need positive numbers for past midnight. Not sure how to best set up the Beeminder goal in a “thresholdy” way (so that it derails you if one single value is above a threshold), though.

2 Likes

Also, I had to tinker a bit to find out which data fields are available from the Oura API. If anyone wants to build a similar goal, these are the ones I could find:

Sleep:

summary_date, period_id, is_longest, timezone, bedtime_end, bedtime_start, breath_average, duration, total, awake, rem, deep, light, midpoint_time, efficiency, restless, onset_latency, hr_5min, hr_average, hr_lowest, hypnogram_5min, rmssd, rmssd_5min, score, score_alignment, score_deep, score_disturbances, score_efficiency, score_latency, score_rem, score_total, temperature_deviation, temperature_trend_deviation, bedtime_start_delta, bedtime_end_delta, midpoint_at_delta, temperature_delta

Activity:

summary_date, timezone, day_start, day_end, cal_active, cal_total, class_5min, steps, daily_movement, non_wear, rest, inactive, low, medium, high, inactivity_alerts, average_met, met_1min, met_min_inactive, met_min_low, met_min_medium, met_min_high, target_calories, target_km, target_miles, to_target_km, to_target_miles, score, score_meet_daily_targets, score_move_every_hour, score_recovery_time, score_stay_active, score_training_frequency, score_training_volume, rest_mode_state, total

Basically, every raw metric you can see on the Oura app and maybe even more.

2 Likes

@bee I used

#!/usr/bin/env python3

from __future__ import annotations

from datetime import datetime, timedelta

import oura
import pandas as pd
import pytz
from pandas import DataFrame
from pyminder.pyminder import Pyminder

WINDOW_SIZE: int = 7

pandas_client = oura.client_pandas.OuraClientDataFrame(
    client_id="",
    client_secret="",
   personal_access_token="",
)


def get_data() -> DataFrame:
    start = datetime.now()
    end = start - timedelta(days=WINDOW_SIZE)

    sleep_stats: DataFrame = pandas_client.sleep_df(
        start=start.strftime("YYYY-MM-DD"),
        end=end.strftime("YYY-MM-DD"),
    )
    # start delta is number of seconds since midnight of listed days that bedtime started
    sleep_stats[["sleep", "wake"]] = sleep_stats[
        ["bedtime_start_delta", "bedtime_end_delta"]
    ].applymap(lambda x: timedelta(seconds=x) / timedelta(hours=1))
    sleep_stats.index = pd.DatetimeIndex(
        sleep_stats.index, tz=pytz.FixedOffset(sleep_stats.timezone[0])
    )
    return sleep_stats[
        ["sleep", "wake", "bedtime_start_dt_adjusted", "bedtime_end_dt_adjusted"]
    ]


sleep_stats = get_data()

beeminder = Pyminder(user="", token="")

sleep_goal, wake_goal = beeminder.get_goal("oura-sleep"), beeminder.get_goal(
    "oura-wake"
)

for summary_date, (sleep, wake, start, end) in sleep_stats.iterrows():
    sleep_goal.stage_datapoint(
        value=sleep, time=summary_date.timestamp(), comment=f"Sleep time is {start}"
    )
    wake_goal.stage_datapoint(
        value=wake, time=summary_date.timestamp(), comment=f"Wake time is {end}"
    )

sleep_goal.commit_datapoints()
wake_goal.commit_datapoints()

in a similar way as @howtodowtle, except with positive values and 2 do-less goals.

How could I use the graph editor to set (and forget) a schedule? (sleep and wake earlier by 1 min every day for a month, then 3 minutes earlier each day).

My current sleep time is 2 am and wake time is 10:30 am. I want to dial it to 11pm and 7:30 am gradually with the aid of oura’s autodata.

1 Like