Reverse Engineering the Ryobi API for Homebridge

The Goal: 

I recently set up a HomeBridge server in my house, and have successfully integrated almost every 'smart' device into the Home App on my iPhone, with one exception.  My garage door.

Ryobi Garage Door - GD126

(Quiet Compact 1-1/4 HP Belt Garage Door Opener)

I was determined to get Garage Door integration into HomeKit, and I wasn't going to give up until I made it work. 

First Attempt: 

I set up HomeBridge using HOOBS so the first thing I did was search for a Ryobi GDO plugin in NPM. 

Here’s what I found. 

homebridge-garagedoor-ryobi

https://www.npmjs.com/package/homebridge-garagedoor-ryobi

I installed the plugin and entered my Ryobi credentials, no dice. No response in the Home app, and in the HOOBS logs, I was getting an error with the plugin communicating with the garage door. 

Some light googling showed that every plugin I came across was written specifically for the GDO200, not my GD126. At first, I didn’t think this would be a big problem, but I was wrong. 

Experimenting: 

All of my research was based off of the great work found here: 

Ryobi Garage Door API(???)

https://yannipang.com/blog/ryobi-garage-door-api/

This laid the groundwork for everything I did to get this working. 

I started by authenticating via WebSocket request at https://websocket.org/echo.html using the auth request found on the website listed above.

{"jsonrpc":"2.0","id":3,"method":"srvWebSocketAuth","params": {"varName": "EMAIL","apiKey": “API_KEY"}}

This was received by the server and I was authenticated. We’re getting somewhere. 

Here’s where I ran into problems, he’s using a GD200 and I’m on a GD126 so when I used the Open/Close commands the server rejected the request. I needed to find out how to format this request to work with my GDO. 

Here’s where I started. 

Open Door

I broke this request down into its separate parts. 

 {“jsonrpc”:"2.0",

 “method”:”gdoModuleCommand”, < Should stay the same

 “params”:{“msgType":16, <— Should stay the same

 “moduleType”:5, < May not correlate with GD126 

 “portId”:7, < May not correlate with GD126

 “moduleMsg”:, < Most likely the same

 “topic":"GARAGEDOOR_ID"}} <— Need to extract this from the API

Finding my GarageDoor ID:

So I had to find my Garage Door ID (“Topic”). 

I started by opening this URL in Firefox. 

 https://tti.tiwiconnect.com/api/devices?username=RYOBI_EMAIL&password=RYOBI_PASSWORD

From there I got the device ID from the resulting JSON file. (Although I didn’t know it at first)

(If you’re trying to find your Device ID, go to that URL and look for varName under [1]:)

varName SS

After spending a long time sending WebSocket commands through https://websocket.org/echo.html with my newly found device ID, all I was getting was failures. No joy whatsoever. 

At this point I figured the device ID was incorrect, but in reality it was the moduleType and portId the entire time. I didn’t learn this until much later. 

The Compromise:

At this point I was itching to make SOMETHING that worked.

I ended up isolating the relay in a Sonoff smart switch, then wiring it into the tac switch on the garage door opener in my car. All this did was simulate a button press on the garage door opener when I activated the Sonoff switch.

This worked, but it was in no way elegant.

This worked, but it was in no way elegant.

At this point I was feeling semi accomplished, because I could now open my Garage via the Home app, BUT I couldn’t see the state of the garage door, and more importantly, I couldn’t open and close my garage door via CarPlay because it was a simple switch in the Home app, not an actual garage door.

This was enough to get me to go deeper. I needed to investigate the Ryobi API myself.

Digging into the API:

I have never done anything like this, and I had no clue what I was doing. Fortunately I was determined, and I eventually figured it out. Here’s what I did.

Goal:

Figure out what messages the Ryobi GDO app is sending over the network when opening and closing the garage door.

The Plan:

So I wanted to intercept the messages between the Ryobi app and Ryobi server to see what WebSockets messages it is sending to my garage door.

I know I can proxy the traffic from my iPhone through Charles on my Mac and intercept the messages there, but I knew it wouldn’t work because according to Yanni Pang’s website, he had to bypass SSL pinning to monitor the traffic coming from the app.

I don’t have an iOS device that can be jailbroken, and I have never even used an Android device so I figured I would try to emulate it. I set up countless VirtualBox sessions with a myriad of Android versions (Thanks to https://www.android-x86.org).

I ran into so many issues, and through an extremely long and arduous process of elimination, I actually settled on “Nox App Player”. This is an Android emulator meant for playing Android games on the Mac/PC. This emulator has a root option built in which made the process much easier.

At this point I was able to install Xposed Framework (https://repo.xposed.info/module/de.robv.android.xposed.installer) and SSLUnpinning (https://repo.xposed.info/module/mobi.acpm.sslunpinning).

I installed the Ryobi GDO app, proxied traffic through my Mac using Charles, installed the Charles cert in my Android emulator, targeted the Ryobi app with SSLUnpinning, and the app froze.

Looking in Charles showed that it was not authenticating the connection, and closing out. After all this time I was starting to feel really discouraged. I didn’t know what I was doing, and I was starting to feel like I wasn’t going to be able to figure this out. Something wasn’t working here.

Digging In:

At this point I knew that I had to get around SSL Pinning, and I didn’t know how, or why the SSLUnpinning module wasn’t working. I did a bit of research and decided to go with the hardest, most unfamiliar route.

I needed to unpack the APK and bypass it myself. Here we go.

Breaking the Ryobi GDO App:

I did quite a bit of research and came up with a game plan after coming across this:

https://developer.android.com/training/articles/security-config

I needed to make a network security configuration file for the app to use, allowing me to use a user installed CA. This seemed to be the easiest way to do it.

I created the XML file per the documentation:

<network-security-config> 
   <debug-overrides> 
     <trust-anchors> 
       <!-- Trust user added CAs while debuggable only -->
       <certificates src="user" /> 
     </trust-anchors> 
   </debug-overrides> 
</network-security-config>

I then unpacked the APK with apktool. According to the documentation, this network security configuration needed to go in

res/xml/network_security_config.xml

apk source

This is the file structure that was extracted from the APK. I created the directory within the structure and then I needed to modify the “AndroidManifest.xml” file to read the newly created XML file.

application tag
android:networkSecurityConfig="@xml/network_security_config"

I appended the text above to the end of the <application> tag within the XML file so it would accept my new network configuration.

At this point I repacked the APK with apktool, and signed it with jdk keysigner and jartool with the help of this document (https://blog.bramp.net/post/2015/08/01/decompile-and-recompile-android-apk/).

Moment of Truth:

At this point, I loaded the app into my android emulator, set up the Charles proxy and loaded up my newly repacked APK. Bingo. All the websocket traffic was now sitting in front of me. I literally jumped out of my chair and ran around my house overflowing with pride and sheer joy.

Success.

Success.

Sending the command to open/close the garage door via the emulated app and monitoring the websocket commands.

Sending the command to open/close the garage door via the emulated app and monitoring the websocket commands.

Communicating with the GDO:

Now that I had intercepted the commands, all I had to do was follow in Yanni Pang’s footsteps and make some scripts to interface with the homebridge-garagedoor-command (https://github.com/apexad/homebridge-garagedoor-command) HomeBridge plugin.

open.js

const WebSocket = require('ws');

const ws = new WebSocket('wss://tti.tiwiconnect.com/api/wsrpc');

ws.on('open', function open() {
    
                  // Sending data specific to GD126
                ws.send('{"jsonrpc":"2.0","id":3,"method":"srvWebSocketAuth","params": {"varName": "RYOBI_EMAIL","apiKey": "RYOBIAPIKEY"}}'); 
});
                
              
                   ws.on('message', function incoming(data) {
                       
ws.send(''); 
ws.ping();
console.log('OPENING');
});

ws.on('pong', function pong() {
    ws.terminate();
});

close.js

const WebSocket = require('ws');

const ws = new WebSocket('wss://tti.tiwiconnect.com/api/wsrpc');

ws.on('open', function open() {

                  // Sending websocket commands for GD126
                ws.send('{"jsonrpc":"2.0","id":3,"method":"srvWebSocketAuth","params": {"varName": "RYOBI_EMAIL","apiKey": "RYOBIAPIKEY"}}'); 
});


                   ws.on('message', function incoming(data) {

ws.send(''); 
ws.ping();
console.log('CLOSING');
});

ws.on('pong', function pong() {
    ws.terminate();
});

state.js

var request = require('request');
// Change these variables
        var email = 'RYOBI_EMAIL'
        var pass = 'RYOBI_PASSWORD'
        

    const doSomething = () => new Promise((resolve, reject) => {
         var requestmsg = ''
        var requestmsg = requestmsg.replace('emailhere', email)
        var requestmsg = requestmsg.replace('passwordhere', pass)
        var requestmsg = JSON.parse(requestmsg)
var options = {url:'https://tti.tiwiconnect.com/api/devices/GARAGEDOOR_ID ',method:'GET',json:requestmsg} 
           request(options, (err, res, body) => {
        if (err) return reject(err)
        resolve(body)
    })
})

const someController = async function() {
    var someValue = await doSomething()
                var doorval = someValue.result[0].deviceTypeMap.garageDoor_4.at.doorState.value
                if (doorval === 0) {
            gdoState = "CLOSED";
        } else if (doorval === 1) {
            gdoState = "OPEN";
        } else if (doorval === 2) {
            gdoState = "CLOSING";
        } else {
            gdoState = "OPENING";
}
        console.log(gdoState)

}


someController()

The above scripts were lifted directly from https://yannipang.com/blog/ryobi-garage-door-api/ and modified for the GD126. I did not write these scripts, I only modified them to suit my application.

# Authenticate with server 

{"jsonrpc":"2.0","id":3,"method":"srvWebSocketAuth","params": {"varName": "RYOBI_EMAIL","apiKey": "APIKEY"}}

# Close Garage Door 

{"jsonrpc":"2.0","method":"gdoModuleCommand","params":{"msgType":16,"moduleType":11,"portId":4,"moduleMsg":{"doorCommand":0},"topic":"GARAGEDOOR_ID"}}

#Open Garage Door

{"jsonrpc":"2.0","method":"gdoModuleCommand","params":{"msgType":16,"moduleType":11,"portId":4,"moduleMsg":{"doorCommand":1},"topic":"GARAGEDOOR_ID"}}

I would not have been able to do any of this without all the hard work that Yanni Pang put into researching the API, but I hope that this can help anyone that is looking to integrate the GD126 into HomeBridge.

Resources:

https://yannipang.com/blog/ryobi-garage-door-api/

https://github.com/Madj42/RyobiGDO

https://community.smartthings.com/t/ryobi-modular-smart-garage-door-opener/44294

https://github.com/apexad/homebridge-garagedoor-command

https://blog.bramp.net/post/2015/08/01/decompile-and-recompile-android-apk/

https://repo.xposed.info/module/de.robv.android.xposed.installer

https://repo.xposed.info/module/mobi.acpm.sslunpinning