On Withings & making my daily sleeping patterns public
Some History
In 2008, a French consumer electronics company called Withings1 was founded. They focused on health tech and made what you would call smart or connected hardware. In 2017, Withings sold its business and brand to Nokia. One year later, Nokia announced that it would sell the business back to the co-founder of Withings. This back-and-forth wasn’t surprising to anyone that used their products—to list a few missteps that Nokia made:
- They replaced the Withings Health Mate app with a Nokia branded version—which meant they introduced breaking changes and removed features that resulted in so many frustrated users that Nokia publically announced they regretted the release.
- They discontinued the old Withings watches, including the flagship model which I own. They also discontinued most of the accessories, which meant that if people wanted a replacement wristband they’d need to go with a third party or somehow find an original from eBay or elsewhere.
- They disabled a key feature of its priciest scale.
They also redesigned the watches which in my opinion is another major misstep. But I’ll let you be the judge
From this
To this
With a HR monitor and screen, battery life has decreased (although it’s still great). Regardless, let’s discuss the device I have which I think follows the less is more approach a lot closer.
The Hardware
I bought the now discontinued Withings Activité Sapphire about 1.5
years ago. A few reasons for why I still use it as my main watch
- For what it’s worth, it’s passed the minimum criteria to be called Swiss Made
- It has a scratchproof sapphire glass face
- Battery lasts ~8 months (an easily replaceable standard
CR2025
battery) - It’s lowkey in design with respect to its tech, so it looks like it’s just a regular watch
- There’s a vibration feature that acts as an alarm (however it has never woken me up successfully)
- It tracks activity and sleep
The last point on sleep is what’s relevant to this post. Sleep tracking for the device has been accurate, or at least accurate enough to know I’m not getting enough sleep. I presume it’s based on movement since it can also tell you how many times you’ve woken up during a deep sleep.
The Health Mate API
When the API was first developed, it used OAuth 1.0
for authentication. They recently (July 2018) updated the API to be fully functional with OAuth 2.0
, and what I’m also hoping fixed a few bugs.
I developed the webapp in 2017 using the original API and it worked well most of the time, but soon after the Nokia rebranding they broke new token generation for me so I took it offline. I wasn’t the only one experiencing difficulties, there is a GitHub gist on the API’s limitations by the user kayemonkeys
called Everything Wrong With The Withings API (it’s a critical read).
One good thing about the new OAuth 2.0
API is that at least the documentation is a lot better than before, and token generation actually works so I was able to take the webapp back online.
Getting Sleep
I’ll spare the details of creating a Nokia Health developer app with your account, and generating an access and refresh token since they finally documented the process well enough for people to follow.
Now what I’m looking for is a summary of my sleep for the day—luckily they have a Getsummary
endpoint.
The lastsummary
parameter is a timestamp of the last modified data retrieved in a precedent call. Setting it to 0
retrieves the first call, so I’ll set it to that to grab the latest summary data (in this case for the previous day’s sleep). The startdateymd
and enddateymd
parameters can be ignored since we’re using lastsummary
.
Now curl
ing the endpoint
$ curl -s -X GET 'https://api.health.nokia.com/v2/sleep action=getsummary&access_token=XXXXXXXXXXXX&lastupdate=0'
will return
{
"status": 0,
"body": {
"series": [
{
"id": 676918748,
"timezone": "Australia\/Sydney",
"model": 16,
"startdate": 1511190180,
"enddate": 1511215920,
"date": "2017-11-21",
"data": {
"wakeupduration": 660,
"lightsleepduration": 11640,
"deepsleepduration": 11520,
"wakeupcount": 3,
"durationtosleep": 480
},
"modified": 1511221658
},
.
.
.
{
"id": 831319563,
"timezone": "Australia\/Sydney",
"model": 16,
"startdate": 1531580880,
"enddate": 1531618380,
"date": "2018-07-15",
"data": {
"wakeupduration": 2280,
"lightsleepduration": 16560,
"deepsleepduration": 18660,
"wakeupcount": 1,
"durationtosleep": 0,
"durationtowakeup": 840
},
"modified": 1531643973
}
],
"more": false
}
}
We obviously want the last element of the array since it has the latest data. The relevant data for my use-case is
"date": "2018-07-15"
"lightsleepduration": 16560
"deepsleepduration": 18660
"wakeupcount": 1
now I could show more data like durationtosleep
and durationtowakeup
but I feel information like that is too granular and potentially creepy. Feeling okay with sharing these values, we have all the information we need so let’s make a node
app.
UglySleep.today
Yeah, that’s the domain I went with.
Let’s grab the data and do the math
var sleepdata = function() {
request('https://api.health.nokia.com/v2/sleep?action=getsummary&lastupdate=0&access_token=' + accesstoken, { json: true }, (err, res, body) => {
if (err) { return console.log(err); }
var idx = body.body.series.length-1
var lightsleep = body.body.series[idx].data.lightsleepduration;
var deepsleep = body.body.series[idx].data.deepsleepduration;
var waketimes = body.body.series[idx].data.wakeupcount;
var sleepdate = body.body.series[idx].date;
sdata = [((lightsleep+deepsleep)/60/60).toFixed(1).toString(), waketimes.toString(), sleepdate.toString()];
});
}
Note that:
- The
lightsleep
anddeepsleep
values are returned asintegers
, with unit of time as seconds (a peice of information they haven’t carried over in their new documentation) - The generated
accesstoken
expires every 4 hours, so we’ll use theRefresh Token
endpoint to generate a newaccess_token
every hour - We need to pass in
client_id
,client_secret
, andrefresh_token
as environment variables for theRefresh Token
endpoint
Using node’s ejs to inject the sdata
onto the webpage, and node’s cron to run this periodically, we have the our full server.js
file
const express = require('express')
const app = express()
const request = require('request')
app.set('view engine', 'ejs')
app.use(express.static('public'))
clientid = process.env.CLIENT_ID
clientsecret = process.env.CLIENT_SECRET
refreshtoken = process.env.REFRESH_TOKEN
sdata = []
function sleepdata(accesstoken) {
request('https://api.health.nokia.com/v2/sleep?action=getsummary&lastupdate=0&access_token=' + accesstoken, { json: true }, (err, res, body) => {
if (err) { return console.log(err); }
var idx = body.body.series.length-1
var lightsleep = body.body.series[idx].data.lightsleepduration
var deepsleep = body.body.series[idx].data.deepsleepduration
var waketimes = body.body.series[idx].data.wakeupcount
var sleepdate = body.body.series[idx].date
sdata = [((lightsleep+deepsleep)/60/60).toFixed(1).toString(), waketimes.toString(), sleepdate.toString()]
})
}
function gettoken() {
request.post(
{
headers: {'content-type' : 'application/x-www-form-urlencoded'},
url: 'https://account.health.nokia.com/oauth2/token',
form:
{
'grant_type': 'refresh_token',
'client_id': clientid,
'client_secret': clientsecret,
'refresh_token': refreshtoken,
},
json: true,
},
(err, response, body) => {
if (err) { return console.log(err); }
sleepdata(body.access_token)
refreshtoken = body.refresh_token
})
}
gettoken()
var CronJob = require('cron').CronJob
new CronJob('*/30 * * * *', function() {
gettoken()
}, null, true, 'Australia/Sydney')
app.get('/', (req, res) => {
res.render('index.ejs')
})
app.listen(process.env.PORT || 5000)
Nice and simple. Now we just write some CSS
, make a cute favicon.ico
, and we have our app.
And here’s the GitHub repo.
Deploying
Creating a multi-stage Dockerfile
for deployment on ZEIT
FROM node:carbon-alpine AS builder
WORKDIR /app
COPY package.json server.js /app/
COPY views /app/views/
COPY public /app/public
RUN npm install
FROM node:carbon-alpine
COPY --from=builder /app /app
RUN chown -R node:node /app/
USER node
WORKDIR /app
EXPOSE 5000
CMD ["node", "server.js"]
Then all I need to do is run now -e [ENV_VAR]
, and then now alias
with my custom domain name. Zeit’s documentation on node deployment and custom domains are easy to follow, and OSS SSL deployments are free.
On Privacy
Anyone can now easily check how much sleep I had the previous day by visiting uglysleep.today. Now I’m a private guy but I feel okay with this—I’m not sure why. Perhaps it’s because I know the data isn’t completely accurate, or that it’s the previous day’s data and ephemeral in how it’s presented, or that it’s only a total number of hours sleep, or maybe because it’s presented in a fun and silly website that I chose to share, and have the ability to pull the plug on whenever I want?
However, I can think of a few potential issues. Say I lie to someone by telling them I didn’t get enough sleep—well, now they can verify my lie if they know the website. Or say if someone wants to log sleep metrics over a long period of time, it would be trivial to automatically log the data from the website every day into a database or file.
Whatever the case, I haven’t had an issue (yet).
- Pronounced why-things [return]