5 Comments

Building a Siri/iOS HomeKit-Enabled Garage Door Control with Raspberry Pi – Part 2: Software

This post is the second in a series exploring home automation using a Raspberry Pi, each covering a different aspect of the build of a garage door controller:

  1. Basic hardware acquisition and installation into a project box
  2. Software installation and code for controlling the door
  3. Software camera configuration and code streaming video to HomeKit
  4. Installing and configuring adding door open/closed sensors and adding an LCD status display

This part assumes that you have some basic knowledge of JavaScript, Node, and Linux. It also assumes that you have a Linux distribution up and running on your Raspberry Pi. I used Raspbian Jessie Lite, which is available through NOOBS.

Software Installation

Since we’ll be using Node.js as our JavaScript runtime, you’ll want to download the latest version. I downloaded the ARMv7 version for my Raspberry Pi 3. If you’re using an earlier version of the Raspberry Pi, you may need to choose a different version.

I extracted the archive into my home directory and then added ~/node-v7.1.0-linux-armv7l/bin to my $PATH variable to make it readily available.

Project and Dependency Setup

First, we’ll need to define our dependencies. I created a package.json that includes all the packages that are needed. You can also use npm install --save <package-name> or yarn add <package-name> to generate the contents of this file.

Note the babel packages–we’ll be using ES6 in this project. Also note that we’ll be using a forked version of rpi-gpio that allows us to ‘unexport’ GPIO pins.

{
  "name": "garage-pi",
  "version": "1.0.0",
  "description": "",
  "main": "src/main.ts",
  "scripts": {
    "controller": "DEBUG=controller:* babel-node src/main.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT",
  "dependencies": {
    "babel-cli": "^6.24.1",
    "babel-plugin-transform-async-to-generator": "^6.24.1",
    "babel-polyfill": "^6.23.0",
    "babel-preset-es2015": "^6.24.1",
    "debug": "^2.6.6",
    "hap-nodejs": "^0.4.25",
    "rpi-gpio": "https://github.com/jordancn/rpi-gpio.js.git"
  }
}

After creating our package.json, we can use yarn or npm install to fetch and install the dependencies we specified in package.json.

~/garage-pi$ yarn
yarn install v0.24.4
[1/5] Resolving packages...
[2/5] Fetching packages...
warning fsevents@1.1.1: The platform "linux" is incompatible with this module.
info "fsevents@1.1.1" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/5] Linking dependencies...
[4/5] Building fresh packages...
[5/5] Cleaning modules...
Done in 104.06s.

Controller Code

Next, we’ll write some code in src/main.js that serves as our entry point and handles the registration of our controller as a HomeKit accessory. We’re using the excellent HAP-NodeJS project.

There’s a lot of boilerplate here, but the important point is to hook it up to our garage controller (src/garage.js). We’re importing this as garageController in our entry point. The outline follows the basic garage door controller example provided in the HAP-NodeJS project.

import { Accessory, Service, Characteristic, uuid } from 'hap-nodejs';
import storage from 'node-persist';
import doorController from './door';
import config from '../config.json';
const debug = require('debug')('controller:main');

storage.initSync();

debug(`accessory name: ${config.door.accessory.name}`);
debug(`accessory username: ${config.door.accessory.username}`);
debug(`accessory pincode: ${config.door.accessory.pincode}`);
debug(`accessory port: ${config.door.accessory.port}`);

async function controller() {
  const doorUUID = uuid.generate(`hap-nodejs:accessories:${config.door.accessory.name}`);
  const doorAccessory = exports.accessory = new Accessory(config.door.accessory.name, doorUUID);


  // Door Accessory

  doorAccessory
    .getService(Service.AccessoryInformation)
    .setCharacteristic(Characteristic.Manufacturer, 'Manufacturer')
    .setCharacteristic(Characteristic.Model, 'Model')
    .setCharacteristic(Characteristic.SerialNumber, 'Serial Number');

  doorAccessory.on('identify', function (paired, callback) {
    doorController.identify();

    callback();
  });

  const doorState = () => doorController.isDoorOpened()
    ? Characteristic.TargetDoorState.OPEN
    : Characteristic.TargetDoorState.CLOSED;

  const initialDoorState = await doorState();

  debug('initial door state', initialDoorState);

  doorAccessory
    .addService(Service.GarageDoorOpener, 'Garage Door')
    .setCharacteristic(Characteristic.TargetDoorState, initialDoorState)
    .getCharacteristic(Characteristic.TargetDoorState)
    .on('set', async function(value, callback) {

      if (value == Characteristic.TargetDoorState.CLOSED) {
        doorAccessory
          .getService(Service.GarageDoorOpener)
          .setCharacteristic(Characteristic.CurrentDoorState, Characteristic.CurrentDoorState.CLOSING);

        callback();

        await doorController.openDoor();

        const doorState = await doorState();


        doorAccessory
          .getService(Service.GarageDoorOpener)
          .setCharacteristic(Characteristic.CurrentDoorState, doorStaet);
      }
      else if (value == Characteristic.TargetDoorState.OPEN) {
        doorAccessory
          .getService(Service.GarageDoorOpener)
          .setCharacteristic(Characteristic.CurrentDoorState, Characteristic.CurrentDoorState.OPENING);

        callback();

        await doorController.closeDoor();

        const doorState = await doorState();

        doorAccessory
          .getService(Service.GarageDoorOpener)
          .setCharacteristic(Characteristic.CurrentDoorState, doorState);
      }
    });


  doorAccessory
    .getService(Service.GarageDoorOpener)
    .getCharacteristic(Characteristic.CurrentDoorState)
    .on('get', async function(callback) {

      let err = null;

      if (await doorController.isDoorOpened()) {
        debug('door is open');
        callback(err, Characteristic.CurrentDoorState.OPEN);
      } else {
        debug('door is closed');
        callback(err, Characteristic.CurrentDoorState.CLOSED);
      }
    });

  debug('publish door accessory');
  doorAccessory.publish({
    port: config.door.accessory.port,
    username: config.door.accessory.username,
    pincode: config.door.accessory.pincode,
    category: Accessory.Categories.GARAGE_DOOR_OPENER,
  });
}

controller();

Then, we’ll write a simple module src/door.js that represents our garage door controller. Right now, this is simply a wrapper around another couple of modules src/switch.js and src/sensor.js that opens and closes the door via GPIO control and detects if the door is closed. We’ll stub out the door closed sensor for now.

import Switch from './switch';
import Sensor from './sensor';

const debug = require('debug')('controller:door');

const door = {
  openDoor: async function() {
    debug('openDoor');

    Switch.toggle();
  },

  closeDoor: async function () {
    debug('closeDoor');

    Switch.toggle();
  },

  identify: function () {
    debug('identify');
  },

  isDoorOpened: async function () {
    debug('isDoorOpened');

    const closed = await Sensor.isDoorClosed();

    debug('isDoorOpened', !closed);

    return !closed;
  }
};

export default door;

Next, we’ll add another module, src/switch.js which actually controls the opening/closing of the door by controlling the GPIO pins on the Raspberry Pi.

import gpio from 'rpi-gpio';
import config from '../config.json';
const debug = require('debug')('controller:switch');

const togglePin = config.door.pins.toggle;

gpio.setMode(gpio.MODE_RPI);

const timeout = async (ms) => {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const toggle = async () => {
  try {
    // initial unexport the pin
    debug(`unexport GPIO pin ${togglePin}`);
    const initialUnexportError = await gpio.unexportPin(togglePin);
  } catch (errors) {
    // do nothing
  }

  try {
    // setup
    debug(`setup GPIO pin ${togglePin}`);
    const setupError = await gpio.setup(togglePin, gpio.DIR_OUT);

    // toggle on
    debug('toggle button on');
    const toggleOnError = await gpio.write(togglePin, 1);

    // timeout: 500 ms
    debug('timeout 500 ms');
    await timeout(500);

    // toggle off
    debug('toggle button off');
    const toggleOffError = await gpio.write(togglePin, 0);

    // timeout: 1000 ms
    debug('timeout 1000 ms');
    await timeout(1000);

    // unexport the pin
    debug(`unexport GPIO pin ${togglePin}`);
    const unexportError = await gpio.unexportPin(togglePin);
  } catch (errors) {
    console.error(errors);
    debug(`unexport GPIO pin ${togglePin}`);
    const unexportError = await gpio.unexportPin(togglePin);
  }
}

export default { toggle }

Then, we’ll stub out the door closed sensor in src/sensor.js:

import gpio from 'rpi-gpio';
import config from '../config.json';
const debug = require('debug')('controller:sensor');

const isDoorClosed = async () => {
  const status = false;

  debug('isDoorClosed', status);

  return status;
}

export default { isDoorClosed };

Configuration

Finally, we’ll add some configuration that allows us to set up a pin code and some other HomeKit accessory details. We’ll also specify the GPIO pin number for toggling the garage door.

We’re using physical pin 11, which corresponds to GPIO 17 that we wired up in part 1 of this series. In case you are using a different pin configuration, I used Pinout.xyz for understanding the PIN mapping. The pincode property is used later when pairing our garage door accessory with the Home app on iOS. Set this to something unique.

I added a configuration file, config.json to define the HomeKit accessory details and controller pin.

{
  "door": {
    "accessory": {
      "name": "Garage Door",
      "username": "01:23:45:67:89:AB",
      "pincode": "876-54-321",
      "port": 51826
    },
    "pins": {
      "toggle": 11
    }
  }
}

Startup and Accessory Pairing

Now, we’re ready to start up our controller.

~/garage-pi$ yarn controller
yarn controller v0.24.4
$ DEBUG=controller:* babel-node src/main.js
  controller:main accessory name: Garage Door +0ms
  controller:main accessory username: C1:5D:3F:EE:5E:FB +10ms
  controller:main accessory pincode: 031-45-154 +5ms
  controller:main accessory port: 51826 +0ms
  controller:main publish +29ms

Then we can pair our new garage door accessory using the Home app on iOS. The screenshots below show the order for pairing your garage door accessory with the Home app.

You should now be able to open and close the door using the accessory button in the Home app. At this stage, the open/closed status is not accurate as we have not created a way to determine it. We’ll add this capability later in the series.

If the door is not opening, check your debug console. Debugging information, which may be of use, is printed to the console.

We’re now moving along in our project. In the next part of this series, I’ll cover installing and configuring a video camera so that we can monitor the garage door visually.