Using Python to get data from your Clairy natural air purifier (Part 2)

After we have seen in part 1 of this tutorial, how to setup Charles to intercept data from apps. Let's concentrate in this part on what information the Clairy app request from their servers.

By now you should have Charles set up correctly and see traffic flowing in from you iPhone. (Check if an attempt to connect to Google via Safari will show up in Charles)

  1. Fire up the Clairy app
    As soon as you do this, you should see various connections in Charles. Be aware, that there will also be all connections that your desktop pc will make. It can be quite a lot. (Dropbox syncing, Mail, Browsers …)
    clairy_connections_api
    As you can see on the screenshot above, there are various connections that pop up, when we open the Clairy app. Most of these are for mobile analytics. The URL http://app-measurement.com for example belongs to Google Analytics. (How often is the app opened, how long does a user stay in the app, Is the user coming back regularly, what buttons does the user click, did the app crash and so on). If you're interested, you could also see what kind of information is tracked exactly. Also it seems like Analytics is not deeply implemented yet, as there are no calls when switching screens in the app.
    What we will focus on for now, will be the entry "http://api.clairy.co".

  2. List all Clairy api calls
    In Charles under "Structure" click on the little arrow before "http://api.clairy.co" and then on the "api" folder. Now in the right window pane on "Summary". You should get a list that looks something like the following:
    clairy-api-call-1

  3. Dissecting the API
    Generally speaking with the help of this kind of API, you get an easy way to communicate with Clairy's servers. You send specific information to Clairy (like the serial number of your plant pot and a date range) and you get back the requested data of your plant pot for the specified calendar range.
    What we can see in Charles are a number of URLs that are called (I've replaced my userID and serial number with 'x'). If you double click in Charles on any of these, you will get more information to the specific call.

clairy-api-getinstantdata

3.1. http://api.clairy.co/api/GetBaseUrl/Init?Version=114
This seems to be a call to get the base url api.clairy.co

3.2 http://api.clairy.co/api/User/profile?UserID=xxxxxxxx-xxxx-xxxx-xxxx
This call loads all basic info about the user and clairy in JSON format. The data should be self explanatory so far.

{
	"user": {
		"Id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx",
		"FullName": "Andreas",
		"Gender": 0,
		"BirthDate": "1900-01-01T00:00:00",
		"Email": "myemail@volderette.de"
	},
	"clairy": [{
		"ID": "xxxxxxx",
		"Label": "PlantyMcPlant",
		"Latitude": 48.1373879,
		"Longitude": 11.5404292,
		"City": "Munich",
		"TreeType": 1,
		"Color": 4,
		"Location": 0,
		"IsDeviceOnline": true,
		"Iaq": 1000.00,
		"FirmwareUpdated": true
	}]
}

3.3. http://api.clairy.co/api/Device/GetLastFirmware
If there is a new firmware, it will be downloaded by this API endpoint.

3.4. http://api.clairy.co/api/Device/GetInstantData?Serial=xxxxxxx
Here we come to the more interesting stuff. There are two types of data in this call:

Timers: The app gives you the possibility to schedule when the fan of the pot should be on. As you can see below, I have two timers set. One for Monday to Friday (--> "ActiveDays": [1, 2, 3, 4, 5]) and one for Weekends (--> "ActiveDays": [6, 7]). It looks like we can have a total of 9 timers, as there are 9 slots in the json feed.

"Instant" data: This is the actual live sensor data that is shown on the home screen of the app. The call gets made regularly to update the homescreen live.
The data includes temperature, humidity, indoor air quality (IAQ) and if the fan is active or not. The range for IAQ can be from 1 - 1000 and is incremented in steps which are of variable width (IAQ of 1000 equals "Very good", IAQ of 750 equals "Good", IAQ of 600 equals "Bad", ... ). This may also explain why I'm in the top range most of the time. In my view these steps are too broad and it would be nice to have the exact value in the app.
clairy-home-screen-1
Generally it seems strange, that the info about timers and live data are grouped together in one call. I can't see the reasoning behind that.

{
	"data": {
		"Timers": [{
			"IsActive": true,
			"StartTime": "2017-09-18T06:10:00",
			"StopTime": "2017-09-18T19:10:00",
			"ActiveDays": [1, 2, 3, 4, 5]
		}, {
			"IsActive": true,
			"StartTime": "2017-09-18T00:00:00",
			"StopTime": "2017-09-18T00:00:00",
			"ActiveDays": []
		}, {
			"IsActive": true,
			"StartTime": "2017-09-18T08:00:00",
			"StopTime": "2017-09-18T18:00:00",
			"ActiveDays": [6, 7]
		}, {
			"IsActive": false,
			"StartTime": "2017-09-18T00:00:00",
			"StopTime": "2017-09-18T00:00:00",
			"ActiveDays": []
		}, {
			"IsActive": true,
			"StartTime": "2017-09-18T00:00:00",
			"StopTime": "2017-09-18T00:00:00",
			"ActiveDays": []
		}, {
			"IsActive": true,
			"StartTime": "2017-09-18T00:00:00",
			"StopTime": "2017-09-18T00:00:00",
			"ActiveDays": []
		}, {
			"IsActive": true,
			"StartTime": "2017-09-18T00:00:00",
			"StopTime": "2017-09-18T00:00:00",
			"ActiveDays": []
		}, {
			"IsActive": true,
			"StartTime": "2017-09-18T00:00:00",
			"StopTime": "2017-09-18T00:00:00",
			"ActiveDays": []
		}, {
			"IsActive": true,
			"StartTime": "2017-09-18T00:00:00",
			"StopTime": "2017-09-18T00:00:00",
			"ActiveDays": []
		}, {
			"IsActive": true,
			"StartTime": "2017-09-18T00:00:00",
			"StopTime": "2017-09-18T00:00:00",
			"ActiveDays": []
		}],
		"Device": null,
		"Serial": "xxxxxxxxx",
		"Date": "2017-09-18T15:54:49",
		"Temperature": 17.80,
		"Humidity": 84.80,
		"FanIsOn": true,
		"Iaq": 1000.00
	}
}

3.5. http://api.clairy.co/api/Device/GetAllData?Serial=xxxxxxx&StartDate=2017-09-18T12%3A17%3A00

Now this is the most interesting API query for our use case. Here we can get data for a specific date range as you can see. What's interesting is, that the data is in 10 minute increments and always starts at the 7th minute of an hour. I wonder why it's not synced to every hour on the hour. The smallest available increment in the app is hourly data, but the 10 min increments probably are there to smooth out the curve…

[{
	"Device": null,
	"Serial": null,
	"Date": "2017-09-18T16:57:00",
	"Temperature": 17.80,
	"Humidity": 83.40,
	"FanIsOn": true,
	"Iaq": 750.00
}, {
	"Device": null,
	"Serial": null,
	"Date": "2017-09-18T16:47:00",
	"Temperature": 17.90,
	"Humidity": 83.60,
	"FanIsOn": true,
	"Iaq": 750.00
}, {
	"Device": null,
	"Serial": null,
	"Date": "2017-09-18T16:37:00",
	"Temperature": 18.00,
	"Humidity": 86.00,
	"FanIsOn": true,
	"Iaq": 600.00
}, {
	"Device": null,
	"Serial": null,
	"Date": "2017-09-18T16:27:00",
	"Temperature": 17.90,
	"Humidity": 86.30,
	"FanIsOn": true,
	"Iaq": 600.00
}, {
	"Device": null,
	"Serial": null,
	"Date": "2017-09-18T16:17:00",
	"Temperature": 18.00,
	"Humidity": 86.30,
	"FanIsOn": true,
	"Iaq": 750.00
}, {
	"Device": null,
	"Serial": null,
	"Date": "2017-09-18T16:07:00",
	"Temperature": 17.90,
	"Humidity": 85.30,
	"FanIsOn": true,
	"Iaq": 750.00
}, {
	"Device": null,
	"Serial": null,
	"Date": "2017-09-18T15:57:00",
	"Temperature": 17.80,
	"Humidity": 84.80,
	"FanIsOn": true,
	"Iaq": 1000.00
}, {
	"Device": null,
	"Serial": null,
	"Date": "2017-09-18T15:47:00",
	"Temperature": 17.80,
	"Humidity": 84.30,
	"FanIsOn": true,
	"Iaq": 1000.00
}, {
	"Device": null,
	"Serial": null,
	"Date": "2017-09-18T15:37:00",
	"Temperature": 17.70,
	"Humidity": 83.90,
	"FanIsOn": true,
	"Iaq": 1000.00
}]

What else can we do with the api?
Apart from the GET calls above, that allow the app to fetch data, there are also POST calls, where the app sends updated settings. With these we could build software to control Clairy without the app. I leave it to you to play with the options here. Just click through all settings in the app to see the specific calls show up in Charles.

  1. Authentication
    Now we have the important API calls there is one more thing to do before we can use the calls in our own Python scripts. If you copy one of the urls from above and open it in your web browser, you will see a prompt asking for a username and a password. Which - of course - we don't have.
    clairy-api-password-prompt-1
    Luckily, our friend Charles will help us out here as well. Actually the Clairy app sends an authorization key with every API call in the header of the request (More information about http headers). Clairy's authorization key is most likely in the format of an oauth 2.0 bearer token.
    To find the token in Charles, just click on one of the API calls and be sure to have "Contents" and "Headers" selected in the tab menu on the right. You can see it on the following screenshot.
    authorization-bearer-clairy
    The interesting part here is - you've probably guessed it already - the http header field "Authorization". Here, we can see the word "Bearer" followed by a very long string of characters and numbers (obfuscated on the screenshot). This token allows us to authenticate an API call from within a Python script without the need for a username or password.

With this information at hand, we conclude part two of this tutorial. In part 3 next week we will see how to put everything together in a Python script. See you then!