Interrupt-driven contact sensors with Homebridge and Raspberry Pi GPIO

R. X. Seger
5 min readOct 11, 2016

--

A brief follow up to Home automation with Raspberry Pi + Homebridge, which focused mostly on control and environmental sensors. What about sensors you can directly interact with?

Shown here is a normally-closed momentary switch. Press the pushbutton (the small white barely-visible nub on the top right above the SWITCH label), or have the attached lever press it, it will produce a satisfying click and open the circuit. It’s just a simple pushbutton switch, nothing out of the ordinary.

More complicated “contact sensors” are available, but they all operate on the principle of detecting when something is opened/closed. Alarm.com describes their purpose in What is a Contact Sensor? as:

Thanks to dramatic improvements in home security technology, the unassuming contact sensor, a staple of security systems, has also become a key building block of the Smart Home. Originally designed to catch an intruder opening a door or window, this small device can also trigger smartphone alerts, turn your lights on, and even control a smart thermostat. […]

Contact sensors tell your system if something is open or closed. They’re typically installed on doors, windows or drawers throughout the house.

You can install them on doors, drawers, cabinets, closets, rugs, or even on control panels for you to directly trigger by hand. Either way, contact sensors are a versatile tool, a must for putting the automation into home automation.

Wiring up switches to GPIO

Many companies sell contact sensors, but I wanted to make my own.

Enter the Raspberry Pi Zero. An inexpensive yet fully-functional single-board computer, complete with a 40-pin general-purpose I/O header. Not all of the pins can be controlled as general-purpose inputs, but many can. On the Linux-based operating system of the Pi Zero you can run Homebridge, as previously covered, to integrate with other devices for automation.

To start out, first wire the switches to available GPIO pins. I used this cluster of pins (refer to the full pinout), since it had an adjacent ground connection:

The switches complete the connection to ground. You could alternatively wire the other end to +3.3V, but there are more grounds available on the 40-pin GPIO port so connecting to ground was more convenient for my purposes. Here’s the schematic:

The switches will drive low when closed (normally closed, not pressed), and float when opened (pressed momentarily). To ensure a logic level 1 is read when the switch is opened, the Pi’s internal pull-up resistors are enabled:

Run this script, press the buttons, and it should show their state:

pi@raspberrypizero:~ $ python swread.py
0 0 0
0 0 0
0 0 1
1 0 0
0 1 0
1 1 1
0 0 0

Where 0 is not pressed, 1 is pressed. This inefficiently polls each second.

Using interrupt-driven I/O for improved performance

Polling is expensive, burning CPU time to check if anything changed when most often it did not. To solve this, interrupts can be used instead. Basically instead of reading the input state at any given time, we instead ask to be notified on the rising (0 to 1) or falling (1 to 0) state transition edge.

The details I’ve already went over in Interrupt-driven I/O on Raspberry Pi 3 with LEDs and pushbuttons: rising/falling edge-detection using RPi.GPIO, which happens to be one of my most popular articles on Medium for some reason, but to recap a script to read GPIO using interrupts looks something like this:

#!/usr/bin/python
import RPi.GPIO as GPIO
import time
switches = [24, 26, 22]
GPIO.setmode(GPIO.BOARD)
GPIO.setwarnings(False)
GPIO.setup(switches, GPIO.IN, pull_up_down=GPIO.PUD_UP)
def handle(pin):
state = GPIO.input(pin)
print pin, state
for pin in switches: GPIO.add_event_detect(pin, GPIO.BOTH, handle)
while True: time.sleep(1e6)

Run the script, and it will output nothing. Press some buttons and it will log their state changes:

24 1
24 1
24 0
24 0
26 1
26 1
26 1
26 1
26 0
22 1

(Note that it is noisy — if problematic, this could be solved by debouncing. Hasn’t been a problem yet in practice in my testing.)

Synchronizing output from interrupt handlers

There is a slight problem with the above script. Try pressing multiple switches rapidly. What happens?

The output gets corrupted, as multiple handlers overwrite each other. Standard output is a shared resource: to avoid conflicts, a lock (computer science) can be used to “serialize concurrent access”:

import threading
write_lock = threading.Lock()
def handle(pin):
state = GPIO.input(pin)
write_lock.acquire()
sys.stdout.write(‘%s %s\n’ % (pin, state))
sys.stdout.flush()
write_lock.release()

This completely solves the concurrent write corruption problem.

Another new Homebridge plugin: homebridge-contactsensor

Now all that’s left is plugging this code into a plugin for Homebridge.

Unfortunately, I ran into the same problem as with homebridge-pwm-fan, where the only complete (including interrupts) GPIO library I could find for Node.js, pigpio, requires running as root therefore making it unsuitable in a plugin. If anyone knows of or wants to develop a Node package analogous to the Python pigpio interface, or the Python RPi.GPIO module, I’d be very interested.

Until then as a quick workaround I use child_process to spawn a helper Python script (watchpins.py), which in turn listens for interrupts and writes to stdout. The Node.js code reads this output and acts on it accordingly.

The remainder of the Homebridge plugin code is straightforward. Creates a Service.ContactSensor, and uses setValue() on the ContactSensorState characteristic. This actively notifies clients of the change, as opposed to passively listening for ‘get’ events, allowing automation to act as needed:

console.log(`pin ${pin} changed state to ${state}`);
const contact = this.pin2contact[pin];

contact
.getCharacteristic(Characteristic.ContactSensorState)
.setValue(state);

This is homebridge-contactsensor:

and an example configuration for ~/.homebridge/config.json:

Activate one of the contact sensors, and you should see its state immediately reflected on the client applications.

Applications

What are these contact sensors good for?

The canonical example is: open a door, turn on a light.

However, as home automation becomes increasingly sophisticated, there are more possibilities available. As for myself, I’m using these three switches to quickly set useful “scenes”. Press switch A, it turns on the lights for reading, switch B for coding, switch C all off for sleeping. The switches are conveniently placed so they will hopefully be useful when I’m away from my other devices. Who needs a computer anyway?

A cool picture of some open doors

Conclusions

In this comparatively brief follow up to the 23-minute-read Home automation with Raspberry Pi + Homebridge we have seen how to add another service to your automated home, the simple yet useful contact sensor, via an interrupt-triggered GPIO input on a Raspberry Pi Zero running Homebridge.

--

--