Skip to content

Reverse Engineering the Duux API

Published: at 10:00 PM (6 min read)

In this blog I will talk about how I found the Duux API from their iOS app and then found the functions to control my heater.

Table of contents

Open Table of contents

iOS app to Swagger URL

Without setting up a proxy, how do you see the network requests an iOS app is making? Luckily Apple include the App Privacy Report to allow just that.

Armed with the endpoints the app hits, I was able to see that the app called v5.api.cloudgarden.nl. Loading this site gave me a early 2000’s flashback and an idea, do they have a Swagger (or OpenAPI) endpoint, and did they give it a redirect URL.

Appending /swagger to the URL gave me my answer, yes! And now we have a starting point.

Password and Passwordless login

The app doesn’t use passwords to login, you get emailed a One Time Passcode to input and authenticate yourself. This is not helpful when looking at how that works, you need to give it some information that it uses to generate the code, then send this again with the code for it to verify.

It was late and I didn’t want to work out how to do this so I had a poke about at other URL patterns. I tried app.cloudgarden.nl and got given a login prompt, needing a user name and password. Clicking Forgot Password I was able to give it my email address I used on the Duux app, and it sent me a link to set my password.

I also found that if you logged in using the /auth/signin/anonymous endpoint and queried the email endpoint it has a link to the same URL, handy.

At this point, I double checked I could still log into the app, and I could. Brilliant. I now I have a way to login to the API and the app.

API login

I think I tried a couple of the auth endpoints but settled on using the /auth/v4/login to give me my bearer token. This (including the Bearer keyword) can be copy and pasted into the Authorize modal triggered from the top of the page.

With some more time, effort and desire, I might have been able to get the passwordless one working, but it didn’t seem necessary at this point.

I have noticed it sends me a 2FA code but it has not needed it, so I have ignored them.

Now were in, what can we find out about our heater?

Finding the Heater

From here it was a case of trying the endpoints to see what I could find out, aiming to find my heater.

I started with the Dashboard endpoints, they were get requests and I assumed that the information they provided would be used to drive the home page of the app, where I can see the heater. This was a bust, it only showed that I had 1 room.

The dashboard endpoint had an optional query param of tennantId, so looking at the /auth/current-user response, I found 2 id’s. There were other people with write ups for the fans that said 44 was the ID for Duux, so the other one must be mine?

The /user endpoint gave me some other information that might be helpful, my user ID, the space ID and name (mine is called office).

Interestingly, the /users/sensor endpoint didn’t return anything, but there was a whole sensor section, and there must be a sensor in the heater so I headed there next.

Jackpot! The /sensor endpoint has a payload with a value in it for “Office Heater” and there is an id and a deviceId, it looks like a MAC address and that is listed as a URL parameter for some of the other endpoints.

Popping the id into /sensor/{sensorId}, we get some more information, but I am still not sure how to control the heater, or get any of its stats back.

I worked my way down the various endpoints that had a GET request, until I got to the last one, /smarthome/sensors. And this is where things got interesting. This one endpoint gave me a schematic for the heater. It details the various commands and where to find their current values in the latestData block.

The data

The fullData block is a bit of a mess (but then again the whole API is…) but looking at the app and the Traits block (this lists all the commands and where to find their requestStatusPath) I think we can work out what is what.

In the traits block I have the following paths.

"latestData.fullData.power" - this is the on/off part, 1 being on, 0 being off
"latestData.fullData.sp" - this is the setpoint, or the target temp
"latestData.fullData.temp" - the current temp reported by the device
"latestData.fullData.night" - if the heater is in night mode (the screen is off), same as power
"latestData.fullData.mode" - the heater has 3 modes, Boost (high plus fan), High and Low

So now we have the values from the heater, but can we control it?

Taking control

I tried a number of endpoints to set the heater, finally finding /sensor/{deviceMac}/commands to be the easiest to get the right payload for.

We are also helpfully given the commands needed to control the heater, with the parameters for the values

tune set power {on} - 1 or 0
tune set sp {temperatureSetpoint} - between 5 and 36, in 1 deg increments
tune set mode {heaterMode} - HEAT (this must be common across their fans?)
tune set night {nightMode} - 1 or 0
tune set mode {mode} - 1 or 0

I have found in testing that some will take variations (such as night off|false|0) working but others won’t (night on or true don’t work, only 1)

And there we have it, I can control my heater without needing their app. Now it is ready to try and make a Home Assistant Integration for it.

All the commands

Lock

{
  "command": "tune set lock 1"
}

Timer

{
  "command": "tune set timer 1"
}

Power and night

This is a strange one, it looks like it needs to be prefixed with a 0 for the on to work

{
  "command": "tune set power 01"
}
{
  "command": "tune set power 00"
}
{
  "command": "tune set night 01"
}
{
  "command": "tune set night 00"
}

Temp

{
  "command": "tune set sp 17"
}

Mode

{
  "command": "tune set heating 0"
}

Footnotes

I am really interested in what the /hire, /cargo/vehicles and /swell/devices are for! They don’t seem to return anything useful or are behind API keys.

There were some endpoints that returned more information than I think they should have. I have reached out to the company to share the details.

This Repo by Noah Evans was very helpful for working out the command format.