date: 2016-04-10 title: Reverse engineering the Sense API description: How to retrieve data from a Hello Sense without using the official apps. area: REST APIs url: /2016/04/10/sense-api/ aliases: [“/2016/04/10/sense-api.html”]
resources:
{{< figure “right” “A Sense unit and its two pillow sensors” >}}
Sense is a little device that sits by your bedside and, in conjunction with a little ‘pill’ attached to your pillow, monitors your sleeping patterns and any environmental conditions that might hamper them. Android and iOS apps show you your sleep history, and offer suggestions for improvements.
Sense was Kickstarted in August 2014, raising over 2.4 million US dollars, and shipped to backers in mid 2015. The campaign blurb included this snippet:
Building with Sense
You’ll always have access to your data via our API. Take it, play with it, graph it, do whatever you want with it. It’s yours. That’s important to us.
We enjoy tinkering with and building on-top of other products we like. Sense will let you have that experience.
We’d love to hear your thoughts on what you might want to build with Sense, and how you could directly interact with the hardware, and the data it collects.
Sounds great! But a year after shipping, there’s no sign of an API, and some of us who enjoy tinkering are getting a bit restless…
{{% update 2019-05-28 %}} Hello, the company behind Sense, shut down in 2017 and took the Sense APIs with it. The information below is no longer relevant. I’m not personally aware of any way to interact with the Sense hardware directly. {{% / update %}}
Data from the Sense hardware is transmitted off onto the Internet somewhere, and then pulled down by the mobile apps. If we can snoop that traffic, we can probably figure out how to grab the data from the Sense.
So the first step is to capture the raw network traffic while the Sense and the app are in use. I crafted an extremely over-the-top setup that involves my phone and the Sense being on a new, separate wireless network that gateways through a Linux server that can look in on all the traffic (because you never know when a dedicated capturing network will come in handy, right?…)
Running tshark
while the app and device are active showed they were making HTTPS requests an
Amazon Elastic Cloud instance. That’s not really much use, as we can’t see the content of the
requests. The next step is to move up the network stack and target the HTTPS traffic specifically.
Step in, mitmproxy
. This automates man-in-the-middle attacks
on HTTPS requests. When it receives such a request, it cooks up its own certificate and sends that
to the client, and then sits in between while the client and the server communicate.
The certificates generated by mitmproxy
aren’t issued by a trusted source, so (well behaved)
browsers and apps will throw up lots of warnings and refuse to communicate with it for the most
part. This is the mechanism that stops anyone impersonating your banking website when you access it
over HTTPS, so is generally a very good thing. In this case, both the Sense itself and the Android
app refused to talk with the blatant attacker.
We control the Android phone, though, so can just tell it that we trust the dodgy certificate issuer. This involves grabbing the certificate generated by mitmproxy, and adding it to Android’s key store. The process is documented nicely in the mitmproxy docs and is pretty straightforward. With Android trusting our certificate authority, the Sense app starts talking through the proxy and we can look at the plain HTTP requests.
Logging in to the Android app and navigating through the various screens shows that the API is
actually pretty nice. It’s RESTful, and uses an OAuth bearer token for authorisation, as you can
see in the mitmproxy
output of the authentication and first few requests:
{{< highlight text >}} POST https://api.hello.is/v1/oauth2/token
← 200 application/json 151B 1.17s
GET https://api.hello.is/v2/account/preferences
← 200 application/json 124B 109ms
GET https://api.hello.is/v1/account
← 200 application/json 181B 388ms
GET https://api.hello.is/v2/devices
← 200 application/json 235B 126ms
POST https://api.hello.is/v1/app/checkin
← 200 application/json 106B 119ms
GET https://api.hello.is/v2/insights
← 200 application/json 1.4kB 157ms
POST https://api.hello.is/v1/notifications/registration
← 204 [no content] 364ms
GET https://api.hello.is/v2/timeline/2016-04-09
← 200 application/json 160B 550ms
GET https://api.hello.is/v1/questions?date=2016-04-10
← 200 application/json 284B 132ms
PATCH https://api.hello.is/v1/app/stats
← 202 [no content] 126ms
GET https://api.hello.is/v1/app/stats/unread
← 200 application/json 71B 171ms
{{< / highlight >}}
To get a bearer token, you send a POST request to /v1/oauth2/token
, supplying the username and
password, and a client ID and secret. Presumably if the API is ever opened up you’ll be able to
register clients and get your own ID and secret, but for now reusing the ones from the Android
app works fine. [I’ve prettified and linewrapped the content to make it a bit easier to read.]
{{< highlight http >}} POST /v1/oauth2/token HTTP/1.1 Host: api.hello.is Accept: / Content-Length: 123 Content-Type: application/x-www-form-urlencoded
grant_type=password &client_id=8d3c1664-05ae-47e4-bcdb-477489590a &client_secret=4f771f6f-5c10-4104-bbc6-3333f5b11b &username=USERNAME &password=PASSWORD {{< / highlight >}} {{< highlight http >}} HTTP/1.1 200 OK Cache-Control: no-cache Content-Type: application/json Date: Sun, 10 Apr 2016 17:25:16 GMT transfer-encoding: chunked Connection: keep-alive
{ “token_type”:“Bearer”, “expires_in”:31536000, “account_id”:“1234”, “access_token”:“2.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx”, “refresh_token”:“2.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy” }
{{< / highlight >}}
Getting the timeline for one night is just a straight up GET request. It gives a detailed log of events, including different ‘depths’ of sleep (I’ve cut out a whole bunch here for simplicity). It also shows metrics relating to the entire night, and their condition (‘warning’ or ‘ideal’).
{{< highlight http >}} GET /v2/timeline/2016-04-08 HTTP/1.1 Host: api.hello.is Accept: / Authorization: Bearer 2.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {{< / highlight >}} {{< highlight http >}} HTTP/1.1 200 OK Cache-Control: no-cache Content-Type: application/json Date: Sun, 10 Apr 2016 17:29:00 GMT transfer-encoding: chunked Connection: keep-alive
{ “score”:76, “score_condition”:“WARNING”, “message”:“You were asleep for 8.2 hours, and sleeping soundly for 3.2 hours.“, “date”:“2016-04-08”, “events”:[
{
"timestamp":1460168700000,
"timezone_offset":3600000,
"duration_millis":60000,
"message":"You went to bed.",
"sleep_depth":0,
"sleep_state":"AWAKE",
"event_type":"GOT_IN_BED",
"valid_actions":["ADJUST_TIME","VERIFY","INCORRECT"]
},{
"timestamp":1460169600000,
"timezone_offset":3600000,
"duration_millis":60000,
"message":"You fell asleep.",
"sleep_depth":100,
"sleep_state":"SOUND",
"event_type":"FELL_ASLEEP",
"valid_actions":["ADJUST_TIME","VERIFY","INCORRECT"]
},{
"timestamp":1460177040000,
"timezone_offset":3600000,
"duration_millis":60000,
"message":"You were moving around quite a bit.",
"sleep_depth":2,
"sleep_state":"AWAKE",
"event_type":"GENERIC_MOTION",
"valid_actions":["VERIFY","INCORRECT"]
},{
"timestamp":1460199000000,
"timezone_offset":3600000,
"duration_millis":60000,
"message":"Good morning.",
"sleep_depth":0,
"sleep_state":"AWAKE",
"event_type":"WOKE_UP",
"valid_actions":["ADJUST_TIME","VERIFY","INCORRECT"]
}
], “metrics”:[
{ "name":"total_sleep", "value":490, "unit":"MINUTES", "condition":"IDEAL" },
{ "name":"sound_sleep", "value":190, "unit":"MINUTES", "condition":"IDEAL" },
{ "name":"time_to_sleep", "value":15, "unit":"MINUTES", "condition":"IDEAL" },
{ "name":"times_awake", "value":2, "unit":"QUANTITY", "condition":"IDEAL" },
{ "name":"fell_asleep", "value":1460169600000, "unit":"TIMESTAMP", "condition":"IDEAL" },
{ "name":"woke_up", "value":1460199000000, "unit":"TIMESTAMP", "condition":"IDEAL" },
{ "name":"temperature", "value":null, "unit":"CONDITION", "condition":"IDEAL" },
{ "name":"humidity", "value":null, "unit":"CONDITION", "condition":"IDEAL" },
{ "name":"particulates", "value":null, "unit":"CONDITION", "condition":"WARNING" },
{ "name":"light", "value":null, "unit":"CONDITION", "condition":"IDEAL" },
{ "name":"sound", "value":null, "unit":"CONDITION", "condition":"IDEAL"}
] } {{< / highlight >}}
Pretty much everything displayed in the app is available as a REST resource. They clearly put some thought into API design, which makes it even more of a shame that they haven’t made it public yet. Some of the other interesting resources are:
/v1/alarms
: the alarms set on the Sense./v1/room/current?temp_unit=c
: gives the latest environmental readouts from the Sense./v1/room/all_sensors/hours?quantity=2&from_utc=<timestamp>
: sensor history./v2/devices
: the paired devices, including battery status of pills./v2/insights
: JSON representation of the cards displayed on the main screen (recommendations,
analysis, etc)./v2/trends/LAST_WEEK
(or MONTH
or 3_MONTHS
): gives graph data for sleep scores, duration
and depths.Now the API is figured out, all that’s left is to build something that uses it…