I have central air-conditioning in my apartment. It’s controlled by a remote, using IR signals to send commands to the A/C control unit.
As any decent geek would, I’d like to be able to control my A/C using other means (e.g., a smartphone).
The last part in the puzzle is making all the pieces play nice with each other, and finally accomplish the intended goal – Controlling the A/C at home with the smartphone from anywhere over the Internet!
How was that accomplished?
An always-on computer in the apartment is running a web server accessible from the Internet, serving an “A/C-control site” (screenshot above from Android smartphone). The site is developed with the Django web framework and Bootstrap front-end framework.
The controls in the web-app are associated with server-side Python functions, that pass the parameters to an RPC server that talks with the Arduino that is connected to its USB port.
The RPC server uses the Arduino to send commands to the A/C based on the parameters it received from the web-app, and uses the microphone to verify that the commands were transmitted successfully.
As usual, the rest of this post provides in-depth description of what I’ve done. The actual projects are available on GitHub (web-app project, and RPC-server project) for anyone to fork, clone, tinker and use.
The front-end web-app
As stated above, the front-end web-app is a Django + Bootstrap (mobile-friendly) site.
The A/C Control part is implemented as a Django app (see code on GitHub), with the root URL (
/) mapped to the home view, the
/#/ URL mapped to A/C
# control form view, and the
/#/command/ URL mapped to the command view for A/C
# (see urls.py).
The form contains two buttons – “Power Toggle” and “Update State”, both send the state set in the form to the A/C-command-RPC-server (the difference being that “Update State” will work only if the A/C is already on, and it will not toggle the power state – I elaborated on that in a previous post).
The reason for that is deployment / security considerations. The RPC server potentially runs on another machine on the internal network, so for the client to be able to send requests directly to the RPC server, that server must also be accessible from the Internet.
In addition, by having the RPC server accessible only from inside the internal network, I can leave it unprotected, concentrating auth-checks in the web-app part. If the RPC server was Internet-accessible, I would have to duplicate the auth-checks from the web-app (which means redundant maintenance), while also increasing the attack surface from the Internet into my internal network (more NATed services == more potentially attacked services).
This pretty much covers everything that goes on with the front-end web-app, at least as far as the A/C control project is concerned.
The attentive reader might notice the other Django app (cam). I don’t intend to go into detail about that one, so I’ll leave it at that: it takes snapshots from the RPC-server webcam, and streams it back to the client. It can be used as a spy cam for instance. Originally I thought I’d use it to detect the status LED on the A/C control unit, but later decided to use beep-detection instead (reasons for that: the image was too low-res, and the LED only reflects on/off status, not state-updates).
The back-end RPC server
The RPC server is also a Django-powered site, that handles requests from the front-end server, and a collection of apps that implement the RPC API’s.
The driving design principle of the RPC server & apps is that it has no persistent state – it receives API requests and returns responses after performing the requested operation.
The “/AC/command/” URL is mapped to a send_command view, that iterates over available AcControl objects (ordered by priority), trying the execute the requested command using the sendCommand method of each one. The command is processed only if all parameters are present in the query string. The iteration stops after the first “successful command execution” (determined by comparing the string response to expected success strings).
The sendCommand method of the AcControl object uses the “writeParam” method to write the power-toggle, A/C mode, fan speed and temperature to the serial port connected to the Arduino, and then invokes the “writeSendAndListen” method to instruct the Arduino to send the command IR signal while using the microphone to listen for a beep confirming the successful transmission and execution of the command.
The result is returned to the view, that subsequently returns it as the API response. The result is a short plaintext string (as JSON) describing what happened with the request. Possible outcomes include:
- Success – I bet you can guess what that means (“true positive”).
- Beep Timeout – The command was sent, but no beep was detected as confirmation. This might mean that the command was not received by the A/C control unit (“true negative”), or that the mic module missed it for some reason (“false negative”).
- Serial Error – Error in communication with the Arduino (e.g. Arduino down or not connected, wrong serial port specified, or some error in opening the port – maybe insufficient privileges, or mismatch in port settings).
- Unsupported Parameter Value … – The specific combination of parameter value does not have a matching preconfigured IR A/C command defined (see a previous post for more on that).
And that concludes the A/C-related RPC-server.
As before, I’ll skip the cam app, but you can probably figure it out yourself in case you’re interested.
Why separate the web-app and the RPC server?
In case you were wondering why the web-app and the RPC-server are two separate projects, I’ll explain it shortly in this section.
The main reason is concerned with deployment considerations:
- The web-app is served from a web server, that can be any machine (physical or virtual) in the network, while the RPC server has to be the machine that is physically connected to the controlled device (Arduino, microhpone, webcam, …). There’s no reason to couple the two functions, so it’s a better design to separate them inherently. (note that there’s nothing preventing you from installing and running both servers on the same machine though, as long as the processes are listening on different ports)
- Let’s say you want to control multiple things, like an A/C, a couple of Raspberry-Pi attached webcams, the TV, etc.. In that case, it makes sense to deploy multiple instances of the RPC-server across multiple devices around the house. But there’s no need for more than a single front-end server. So the separation also makes sense in that respect.
Another reason is concerned with security.
The web server serving the web-app can (and should) run as a dedicated user, with low privileges. The RPC-server (at least the one that communicates with the Arduino) has to run as root in order to open a serial port.
Combining both functions into a single program will force unnecessary elevated privileges for the web server, which has the greatest outward-facing attack surface.
Who can control my A/C?
And speaking about security, when deploying an Internet-accessible remote home control system, the question of access control and restriction is an important one!
Security by obscurity?
Sometimes it might suffice to set up such a system that will be accessible through a fixed IP (or dynamic DNS service), maybe with a non-standard port number, and hope no one finds it (or randomly scans it) and uses it to play with your A/C (or spy on you?).
While my implementation supports such a use-case, it also supports an additional secret password as extra authentication, so only users that supply the
key=secret-pass-string in the URL query string get access to the web-app.
This is implemented as a custom authentication middleware I named sillyauth. The middleware intercepts every request, verifying the
key=? supplied in the query string matches the SILLY_AUTH_PASS string defined in the settings.
A couple of warnings:
- This password protects only Django URLs. This means that static resources that are served directly by the web server (not through Django), and are not protected in any way. This isn’t an issue when using the A/C control app, since there’s nothing sensitive in the static resources.
- The password does not protect /admin/ URLs, since the protected app needs be explicitly aware that it is sillyauth-protected. So use the built-in authentication system for admin…
- The query string might be transmitted in cleartext (depending on web server deployment), so anyone who has passive access to the communication between the client and the server (e.g. anyone on a public Wi-Fi hotspot with you when you access the web-app) can sniff out the password.
- The query string is stored in browser history and bookmarks with no protection.
One of my deployment strategies addresses the issue of password-in-cleartext by enabling SSL (using certificates signed by my private certificate authority).
Updated October 2013: I no longer rely on the sillyauth solution for access restriction for my production deployment. The site is now served using the NGINX web server, which is configured to pass requests to the Django site only after authenticating me based on my Google account, using OAuth 2.0 over SSL.
This solution addresses all of the issues specified above – it protects all URLs, including static / media / admin, nothing passes in cleartext, and the URL and query string contain no sensitive information.
My set up
I guess different setups may experience different reliability levels, so here’s mine, for your reference.
First, how are things set up in my place:
- The web-app is served from an Ubuntu 12.04 x64 virtual machine, running on my ESXi host. The web server is Apache2.
- The RPC server is running on an Ubuntu based laptop, with an Arduino connected to it over USB, and using the built-in microphone.
- The laptop+Arduino are located about 2m from the A/C control unit, in a weird angle, with direct line-of-sight between the IR LEDs and the control unit.
After running the microphone calibration wizard, the system usually works fine – the signals are received correctly, and the laptop recognizes the beeps successfully.
Sometimes that signal doesn’t go through (and a “true negative” is reported thanks to beep detection), and sometimes the signal does go through but the beep isn’t detected (causing a “false negative”) – which is the most problematic case (if I’m away…).
I’d say that the signal goes through over 90% of the time, and beep detection fails once in every 5 beeps in average.
I plan to move the laptop + Arduino on top of a closet near the A/C control unit, once I get that closet there 🙂 Hopefully it will improve the success rate.
OK, that concludes my entire A/C control project.
It was both fun and interesting to plan and execute the project. I definitely learned a lot, going into the project with no prior Arduino experience, or Django/Bootstrap knowledge.
All code is publicly available over on GitHub, and anyone is welcome to download and use, modify and improve, and share your experience with it down in the comments.
Here’s a list of all the posts related to the A/C control project, in case you missed any: