Tuesday, July 18, 2017

Weekend Project: Reverse Engineering a Smart WiFi Dimmer Switch


My wife Kayla and I like to spend our free evenings watching movies & shows, and after dealing with terrible cheaply made universal remotes for years, I finally caved and spent the money on a (refurbished) Logitech Harmony Elite remote/hub to control all of our media devices.

One of the great things about the whole Harmony ecosystem is that it supports a large number of home automation devices like thermostats, dimmer switches, etc… I’ve been interested in home automation stuff for a while, particularly since my cousin Nick showed a few of the things he installed in his house, but we rent a modestly sized two bedroom townhouse and don’t really have a good reason to spend the money and effort on a bunch of sensors, switches, and a potentially expensive hub.

That said, having a scriptable remote that can “talk” to a lot of devices made me want to play with something (...something useful, hopefully!). We decided to get a smart dimmer switch to control our living room lights, the idea being that it would be nice to be able to dim or raise the lights without getting up off the couch.

My criteria were:

  • Cheap (< $70)
  • Didn’t depend on a hub or other devices.
  • Worked with the Logitech remote.

That left me with essentially one option: a Leviton Decora Smart Wi-Fi Dimmer Switch (DW6HD-1BZ), available on Amazon at the time of this writing for $45.
The Amazon order arrived basically the following day. After doing some circuit breaker disco and making a quick walk to our neighborhood hardware store for some wire, the switch was installed and working!

Alas, that 3rd point in the list above ended up being an issue… Logitech’s compatibility page alleged that Leviton Decora devices were supported, but it neglected to mention that only IR ones were - I was out of luck with my wi-fi one. :(


I can be stubborn when it comes to problem solving. In this case, it really rubbed me the wrong way that I had researched, bought, and successfully installed the dimmer, but couldn’t use the remote on it, which was the whole purpose of buying a smart dimmer switch in the first place.

A less masochistic person would have probably returned it and moved on with a stress free life. I, on the other hand, decided to jump directly into packet capture, decompiling, and API hacking.


There were two different issues I had to solve:

  • Switch API. Given that the dimmer was controlled through wi-fi, it had to use some sort of protocol. I needed to understand and then be able to use that protocol.
  • Logitech Harmony API. Assuming that I could control the dimmer, I needed to somehow make the remote be able to interface with it.

I decided to tackle the second point first, since if there was no way to interface with the remote, reverse engineering was pointless. After some preliminary research, it seemed like the way to go would be to create a software wrapper over the dimmer switch that the remote would recognize as some other type of dimmer. That way I wouldn’t have to reverse engineer both sides, only the dimmer.

From what I read, other people have run into the difficulty of creating custom interfaces with the Logitech Harmony ecosystem, and the best thing to simulate seemed to be the Phillips Hue lighting system/bridge, particularly because there were already open source projects that modeled or used their protocol(s).

At first, I envisioned creating my own emulated Hue server Docker container or VM that would talk to the dimmer switch (having no shortage of always-on hardware in the house); but then found that the open source community had given me a much better starting point, namely Home Assistant, an open source home automation platform.

Home Assistant (HA) already had an emulated Hue bridge, as well as a very straightforward way to develop new device platforms. I wasn’t super familiar with Python, but that wasn’t a serious blocker given how similar Python is to Ruby.

I forked and deployed HA locally on my MacBook, configured the emulated Hue bridge, and successfully linked up with the fake bridge using the Harmony remote. So far so good!

The entirety of my HA emulated Hue bridge configuration:

About as simple as it gets.

In this screenshot below, the fake Hue is showing up in my Harmony Elite configuration. The remote found it automatically.

Reverse Engineering

Now we get to the hard part - figuring out how the dimmer switch sends & receives data.
This dimmer switch is designed to be controlled using Leviton’s My Leviton free app, which allows you to configure the wi-fi network it uses, group switches into rooms, rename them, control power and brightness, etc…
The way to go about anything like this is to start simple and obvious, and dig deeper only when necessary - so the first thing I did was just port scan the dimmer switch IP using Mac OS’s very handy Network Utility app (buried under /System/Library/CoreServices/Applications/ in newer versions of Mac OS).

As you can see, this produced a surprising result - the dimmer switch had no open ports!
OK, that’s weird. How does it communicate then? Encouragingly secure, I guess! ;-) Let’s go deeper and do a packet capture. Since I obviously can’t trace the packets on the dimmer itself, the My Leviton app traffic is what I want. Thankfully, Android has a great app called Packet Capture that can even dig into secure SSL/TLS packets (man-in-the-middle attack) if configured with a security certificate.

I turned on packet capture, hooked up an SSL certificate, and tried to log in to the My Leviton app. Unfortunately, it didn’t let me! Oops! That’s because Leviton actually verifies the certificate you’re trying to log in with… bad for breaking in, but a really good thing as far as a security measure. (Good job Leviton!)

OK, but we can still figure out in rough terms how the app communicates with the dimmer even if we can’t see the SSL traffic. Let’s just pass the encrypted packets through and see what we get. After recording and copying over the capture, we crack open Wireshark and get this (top snipped from a larger list):

Ordering by protocol, the only HTTP communication is to which is the harmony hub (Note: the IP addresses for the hub and dimmer swapped due to some router DHCP manipulation I was doing, sorry for the confusion). The rest of the capture, about ~400 packets, is: TCP, TLS, and WebSocket packets to various addresses, NONE of which are the dimmer switch (now
So, the port scanner was telling the truth! During normal operation, the app does not communicate directly with the dimmer switch.

There’s a second clue, implicitly revealed by an advertised feature of the My Leviton app - that you can control the lights in your house when you’re not home. This means that the app doesn’t have to be on the same network as the dimmer to be able to control it. So, here’s how this must work:
There are positives and negatives to this. On one hand, given that the communications have to go through WAN, there's a higher chance that the protocol is fairly high level - HTTP(s) and/or WebSockets, as opposed to some sort of proprietary binary format. On the other hand, there's a much worse likelihood of increased security to stop hackers from taking over unsuspecting homeowners' devices.

Now I had two options. I could either fiddle with security certificates and pursue attempting to break into the protocol through packet capturing the app, or I could go after the code. Packet capture dissection is no fun at the best of times, so I decided to try breaking into the code. The first task was to get the app itself onto my PC.

Android apps are packaged as APKs, which are compressed Java Archives along with any assets the app needs. On phones/tablets themselves, if you don't have root access, there's no good way of getting the APK off because they tend to be located in inaccessible system directories. However, there are a ton of websites that provide the ability to download the APKs directly off the Google Play Store. I won't link to the one I used due to their probable violation of Google's TOS. They're easy to find though.

Next, I used the open source Dex-to-Java tool JADX to decompile the app.

Cool! I was happily surprised to find that the Java code wasn't obfuscated at all, but unhappily surprised to find that there wasn't much Java to read and none of it had anything to do with device communication.

The mystery was quickly solved by looking up Cordova - a 3rd party library that appeared frequently in the codebase. Apache Cordova is a mobile development framework that provides multi-platform support for apps. It does this by delegating UI code to a WebView (essentially client-side HTML/CSS/JS) and providing integration tools to use native platform components. That means that out of the entire contents of the uncompressed APK, there were only two files of interest in the above screenshot: app.js and lib.js. Everything else was framework stuff, assets, or glue code.

Both of the Javascript files looked something like this when opened in a text editor:

Not very appetizing... That said, already some interesting stuff in here, like the words "CONNECTIONS", "CONTROLS", and "EDIT_RESIDENCES". Let's run this stuff through a js beautifier and make it easier to read.

That's much better. Lots of promising stuff here - clearly some API hosts and some "magic" IDs. The other file, lib.js had even more useful looking things like API endpoints and query formats.

Access & Control

Now that we can look through the app code, the first thing to try is logging into their service. Without authorization, we can't control anything. I like Ruby for quick prototyping, so after doing some "Find..." queries for "auth" in app.js and lib.js, I came up with this:

This gave me the following output:
Blurring the IDs just in case... ;-)

Fantastic. So I'm logged in and the service has given me an auth token. If I add that token to the headers for future requests, I'll be authorized to do everything the app does! Let's look up and use some other APIs in lib.js, particularly having to do with switches:

Those are some useful looking API endpoints! I eventually ended up with this Ruby script. Good code? Definitely not. But it works!

Running this script successfully turned on the dimmer switch in my living room and adjusted its brightness to 40%. Success!


Now that I've gained access & control of the dimmer switch, it was time to create a new Home Assistant component that would be linked to the emulated Hue bridge - finally accomplishing the original goal of controlling the dimmer with the Harmony remote.

I won't bore you with the details of me learning Python syntax on the fly, but suffice to say, the final code is very similar. There were a few things I improved from my Ruby prototype:
  • Using a session to communicate with the Leviton APIs. This reduced the request/response overhead and allowed me to just set everything up once.
  • Reconnection. The Leviton authorization codes are only valid for a limited time. Afterwards you have to re-authorize and get a new token.
  • Logging out when Home Assistant is stopped. Let's not leave any open sessions!
  • Usage of Leviton-provided attributes to determine valid dimmer brightness range.
  • Allowing Home Assistant automations to modify on/off transition time (because the APIs supported it)

You can see my final implementation in the pull request I submitted to the Home Assistant repository.

There are a few couple of things I left out either for another time or other implementers:
  • The Leviton APIs include sensors, thermostats, and many other controllable things. The right way to integrate this stuff with Home Assistant would be to write a more global device platform that can manage anything Leviton can access with their app.
  • It appears that the Leviton app can use a WebSocket connection as well as HTTP(s)... I didn't do much digging at this interface since the standard one worked. WebSockets can potentially be more efficient - and either way, having an alternative connection approach may be beneficial.

Finally, here's the dimmer as it looks on our Harmony remote:

Thanks for reading, I hope it was interesting!

No comments: