00:00:20.779
Hi everyone, so this talk is entitled 'Automate Your Home with Ruby'. My name is Karl Entwistle, and you can tweet me at CollinWhistle. I work at Cookpad, and they sponsored me to come to RailsConf and give this talk.
00:00:33.420
You can probably tell from my accent that I'm not from America; I'm actually from the UK. This is my first time in America and also my first time at RailsConf. It's quite an experience, and my brain has associated America with the video game Grand Theft Auto. So, while I'm walking around in the downtown area, I feel like I'm in Grand Theft Auto and everyone else is like an NPC walking past. It's a bit confusing!
00:01:18.270
As I said, I'm from Bristol in the UK, which is a city in the southwest of England. I used to live in London, but my wife and I wanted to buy a house. So, we moved to Bristol, where property prices are much more affordable than in London. We bought a modest-sized home.
00:01:30.690
After moving into our house, I decided that I wanted it to be a smart home, like a smart home from the future. The first thing I bought was some Philips Hue light bulbs. I installed them in all the rooms, and we had to download and install the application onto our phones to control them.
00:01:44.930
With the lighting situation under control, I decided the next thing we needed was to manage temperature with a smart thermostat. Since England can be quite cold, it’s nice to set the heating to come on or off before I leave the office. Again, we had to download and install a separate application for controlling the thermostat. At this point, we ended up with nine different applications on our phones to control various gadgets.
00:02:21.590
There was no consistency between them, nor any interoperability, because the gadgets came from different manufacturers, creating a closed ecosystem. What I really wanted was one unified application to control all my devices.
00:02:34.650
It turns out there is such an application: if you have an iPhone, it's pre-installed, and it's called Home. Home uses a framework called HomeKit. So, what is HomeKit? Apple's website explains that HomeKit is a software framework for making smart home devices work seamlessly with iOS devices. This means you can control your smart home devices using any device in Apple's ecosystem.
00:03:39.690
Since the latest update to the operating system, it also includes Macintosh devices. HomeKit officially supports various devices like lights, power outlets, windows, and fans, among others. What I really like about HomeKit is that you can control it with Siri. You can say commands like 'turn on the living room lights,' 'turn off the lights,' or 'set the dinner scene.' My personal favorite is asking, 'What temperature is the shed?'
00:04:13.710
Normally, the answer would come in an Irish accent, saying the temperature is around 15 degrees Celsius (not Fahrenheit). In addition to Siri, there's also the visual component in the Home app. For example, I can control the brightness of my Philips Hue lights, and I also recently installed underfloor heating, which I can control through the same app.
00:04:43.000
Now, this is the end of the talk, right? You just buy things that support HomeKit? Well, not quite. There is a problem: you can only use devices with HomeKit if they officially support it. You'll know they support it if they have a sticker on the box or it’s listed on the website.
00:05:03.000
In my case, it meant that my Philips Hue light bulbs already worked with HomeKit, but my Nest thermostat and soundbar did not. I could have theoretically gone out and bought all new devices, but that would have been pretty expensive and quite wasteful. So, I had an idea: what if HomeKit could communicate with Ruby? Then I would be able to bridge the gap between HomeKit and any unsupported devices.
00:06:02.130
So, I searched Google and GitHub for a HomeKit Ruby library and was genuinely surprised that one didn't exist. I wondered why; how hard could it be to create a HomeKit library for Ruby? I remembered a quote from Gandhi: 'You must be the change you wish to see in the world.' So, I decided I was going to create the first Ruby library for HomeKit, which I ended up calling RubyHome. You can install it with 'gem install ruby_home,' and it's available on GitHub.
00:06:56.930
I'll now explain how I created the gem, and then I'll show you how you can use it to automate your homes. The first thing I did was go on Apple's developer website and download the non-commercial version of the HomeKit Accessory Protocol specification. I was happy to find out that it was a very technical 256-page document that referenced 12 RFCs.
00:07:15.350
After glancing at the specification, I saw tables of hexadecimal code, a bespoke HTTP protocol, and something about the Cha-Cha 20 Poly 1305 encryption method. At that point, I figured the most logical thing to do was to use an existing JavaScript library. However, I wanted to use Ruby, so I naively thought I could model it in just a weekend. It actually ended up taking about six months of hacking away to produce it.
00:08:08.450
Instead of reading the entire long and technical document, I decided to work through the process iteratively, as if I just bought an officially supported accessory. In the Home app, I proceeded through the workflow of pairing a new accessory. I pressed the plus icon, clicked 'add accessory,' and stated that I didn’t have a code or couldn’t scan. I knew from past experience that the accessory should be displayed there.
00:09:13.350
At this point, I scanned through the specification to figure out how to make the accessory appear. The documentation mentions that accessory servers must support multicast DNS host names, which many Mac or iPhone users are already familiar with as Bonjour. This service helps populate shared devices in the sidebar when you're on a network.
00:09:43.740
So, I found a great piece of software by a company called Tilde, called Bonjour Browser, which displays all the multicast DNS services on your local network. Going back to my Philips Hue bridge, I could see the key-value pairs it was broadcasting using the Bonjour browser. At that point, I just needed a Ruby library to broadcast similar key-value pairs, and I came across a good multicast DNS library for Ruby called DNS-SD.
00:10:50.490
I saw a familiar face associated with it—Aaron Patterson, or Tenderlove. I assumed it would work well, and it has a simple syntax. For example, I required DNS-SD, assigned an instance of a TCP server running on port 80 to a variable, and used the announce function of the DNS-SD library. When I ran that code in IRB and checked it in Bonjour Browser, I could see the service was being advertised on my network.
00:11:55.230
At this point, I began mimicking the key-value pairs that were coming from the Philips Hue bridge. After that, RubyHome appeared as an accessible service on my phone. Upon clicking the icon, I was prompted for a setup code, and I put in a simple one just to get started.
00:12:25.050
Everything up to this point seemed quite standard. However, I noticed something peculiar about the content type of the HTTP POST request, which was 'application/pairing+trv8.' This was unusual because I had primarily worked with JSON or XML APIs.
00:12:39.740
The body of the request contained hexadecimal data, indicating the start of a long and complex pairing process. I don't want to delve into too much detail about the pairing process, but the key is to understand the body of the request so I could proceed.
00:13:03.240
Again, I searched through the documentation for a piece of information related to application pairing plus trv. The documentation specified that pairing responses come as TLV data in the body of the HTTP response. So, I searched further with the new keyword 'TLV,' and I found an entire section detailing it.
00:13:30.800
To interpret the hexadecimal data sent to the server, I discovered a useful gem called BinData. BinData provides a declarative way to read structured binary data, making it easier to manage compared to Ruby's pack or unpack methods. As it turns out, the hexadecimal data was composed of two TLVs, and each TLV contained an 8-bit type followed by an 8-bit length, indicating how much of the value should be read.
00:14:52.780
After figuring out how to read the TLV data, I could proceed with the pairing process. Each device, such as my iPhone or iPad paired with RubyHome, has its own unique token. RubyHome also holds a corresponding unique token for each device. When your phone connects to the network after returning home, the pairing process recurs.
00:15:35.230
The connection holds surprisingly long, and I don’t know why the battery life doesn’t deplete quickly, but it does. When you leave your house, the connection closes, and new tokens are generated next time the device connects to the network.
00:16:16.210
The HAP specification requires a custom HTTP server. When the server and the phone have unique tokens, the connection switches to a customized HTTP protocol, where each HTTP message is split into frames no larger than 1,024 bytes. Each frame has a specific format, and if a connection failure occurs during the session, the server must immediately close the connection.
00:17:03.930
Initially, I found this to be a complicated process and wondered how to implement it. Eventually, I realized it wasn't as complex as it appeared, so let me briefly explain how it works. This is a standard HTTP 1.1 response, which includes the protocol, status code, headers, and a body.
00:17:41.840
I broke the response into three frames: the first two are 1,024 bytes, and the last frame is the remainder. Each of these frames is individually encrypted. The encryption process uses an additional piece of metadata called a nonce, which prevents replay attacks. Specifically, nonce values change with each request, so if someone tried to replay requests, they'd fail.
00:18:19.290
In terms of HTTP handling, I needed to ensure I encrypted the entire response before sending it to the device. I couldn't just encrypt the body or headers individually. Therefore, I substituted the HTTP request and HTTP response classes with my own classes that inherited from the original ones. This allowed me to override parsing logic as needed.
00:19:01.620
Now, when the request is supposed to be unencrypted, I call the original method. Still, when it's meant to be encrypted, I perform the encryption first and then send the entire package over the socket. This approach worked effectively for RubyHome. If anyone has ideas on how to make it better, please talk to me after the session.
00:19:51.510
The last thing I want to cover from the HAP specification is unsolicited HTTP. The documentation explains that accessories can deliver notifications by sending event messages through unsolicited HTTP responses. This means the HTTP server can send responses at any time because the iOS device and server have an open socket connection.
00:20:36.450
For example, if an iPhone requests a door to be opened, RubyHome will broadcast an event to the iPhone and iPad, letting them know that the door is now opened. Another instance is with the thermostat; if someone adjusts the temperature manually, that change gets intercepted through the Nest API, and RubyHome can update all associated devices to keep them in sync.
00:21:43.400
Now that we've covered a lot of technical material, let's lighten things up with some demos. This is all the code needed to create a fan in RubyHome. You can see that by requiring RubyHome and defining the devices as types of services, I've used a service factory to create the fan.
00:22:05.490
When I save and run this code, I'll open the Home app on my iPhone, click on 'add accessory,' state that I don't have a code or can't scan, and start running the program to see the multicast DNS generated code. Then, I can type that code into the phone to proceed with the pairing process.
00:22:42.760
Now, after going through the pairing process, I’ll confirm all the different services. You can see the fan appears, and I can switch it on or off. One cool thing in HomeKit is that different services have unique widgets for interaction.
00:23:16.170
What if we want to know if the fan is switched on or off? In RubyHome, a service can have many characteristics. One characteristic of a fan is the 'on' state, so we can subscribe to changes using a function that yields the updated value.
00:24:06.130
When I run this, I shut down the server and start it again so changes apply. After pairing again, I switched on the fan, and it displayed in the terminal that the fan is on. If I turn it off, it updates accordingly.
00:24:55.190
I can also control it with Siri. For instance, I say, 'Siri, switch the fan on,' and it works flawlessly. Let’s look at how we can reduce the program back to just a few lines of code. Additionally, I want to demonstrate the characteristics the fan has, such as speed and rotation direction.
00:25:50.060
These characteristics are the initial values that get applied each time the server restarts. If I apply this code and restart the server, I can see I now control both speed and rotation.
00:26:45.270
If I want more services than just the fan, I need a bridge. To create a bridge, I include another service called accessory information. If I run this with the fan and add a lightbulb, air purifier, garage door, and others, all these services magically appear upon running the program.
00:27:35.980
Furthermore, all the different characteristics can still subscribe for updates. For example, I created a demo website with a hex color code picker, which simulates a color change using HomeKit through the RubyHome library on my iPhone.
00:28:57.020
Finally, returning to the soundbar, I can switch it on or off with my iPhone and iPad, while showcasing unsolicited events as I'm checking the status from both devices.
00:29:11.380
As stated earlier, there are numerous services you can leverage with RubyHome, and I could demonstrate these for much longer! But I must wrap up somewhere and leave you all inspired. If you’re excited about what you learned today, I hope you consider using RubyHome. You've learned how to break down a large problem into manageable chunks.
00:30:02.800
That’s everything in my talk. Thank you all for listening!