thoughts | THEDEN

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 also redesigned the watches which in my opinion is another major misstep. But I’ll let you be the judge

From this

Withings Activité Sapphire

To this

Nokia Steel HR & Nokia Steel

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

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.

Daily sleep summary data through their WebUI

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.

endpoint: /v2/sleep?action=getsummary

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 curling the endpoint

$ curl -s -X GET ' 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.

Yeah, that’s the domain I went with.

Let’s grab the data and do the math

var sleepdata = function() {
  request('' + 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:

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')


clientid = process.env.CLIENT_ID
clientsecret = process.env.CLIENT_SECRET
refreshtoken = process.env.REFRESH_TOKEN

sdata = []
function sleepdata(accesstoken) {
  request('' + 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() {
      headers: {'content-type' : 'application/x-www-form-urlencoded'},
      url: '',
        'grant_type': 'refresh_token',
        'client_id': clientid,
        'client_secret': clientsecret,
        'refresh_token': refreshtoken,
      json: true,
    (err, response, body) => {
      if (err) { return console.log(err); }
      refreshtoken = body.refresh_token


var CronJob = require('cron').CronJob
new CronJob('*/30 * * * *', function() {
}, null, true, 'Australia/Sydney')

app.get('/', (req, res) => {

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.


Creating a multi-stage Dockerfile for deployment on ZEIT

FROM node:carbon-alpine AS builder

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

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 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).

  1. Pronounced why-things [return]

Written July 2018.

← Building a Local Linux Development Environment with Docker and Make  Running Linux games on MacOS with Docker →