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 [email protected]: The platform "linux" is incompatible with this module.
info "[email protected]" 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.

Conversation
  • Jon says:

    Great post!
    What is your opinion on the Raspberry Pi GPIO GarageDoor plugin for HomeBridge?

    https://github.com/benlamonica/homebridge-rasppi-gpio-garagedoor

    • Jordan Nelson Jordan Nelson says:

      Thank you for the comment. That other project looks pretty cool and could probably work just the same actually. I have a primarily software-only background so I haven’t had much opportunity to dabble in hardware interaction until this project. I really enjoyed the time I spent learning about basic interaction using the GPIO while working on this project.

      • Jon says:

        Cool, keep up the good work!

      • Ted Rust says:

        Sadly, I have diverted pretty far from where this code started me out but there were two issues around the door state code that I think were at the root of this error.

        First, the doorState method needs to be both async and it needs to await doorController.isOpen() for the if-statement to work correctly.

        The other problem may have been around the issue of local doorState overriding the global method doorState. I can’t recall for sure, but this is what my doorState method looks like now:

        const doorState = async () => await doorController.isDoorOpen()
        ? Characteristic.TargetDoorState.OPEN
        : Characteristic.TargetDoorState.CLOSED;

        and I now check the current state of the door at the top of the “set” callback in order to stop this thing from closing an opened door when you say “open my garage” but it’s already open.

        // …snip…
        .on(‘set’, async (value, callback) => {
        let curDoorState = await doorState();

        if (value == Characteristic.TargetDoorState.CLOSED) {

        if ([
        Characteristic.TargetDoorState.CLOSED,
        Characteristic.TargetDoorState.CLOSING
        ].indexOf(curDoorState) < 0) {
        debug('door open, closing…');
        doorAccessory
        .getService(Service.GarageDoorOpener)
        .setCharacteristic(
        Characteristic.CurrentDoorState,
        Characteristic.CurrentDoorState.CLOSING
        );

        callback();

        // … snip …
        curDoorState = await doorState();

        doorAccessory
        .getService(Service.GarageDoorOpener)
        .setCharacteristic(
        Characteristic.CurrentDoorState,
        curDoorState
        );
        }
        else {
        debug('ALREADY CLOSED; staying closed');
        callback();
        }
        // …snip…

        Hope that helps. Oh, btw, I could never get the rpi-gpio module working right and found 'onoff' which was 10000x easier to use and allows for both async and sync calls (and watches a pin for input changes). Highly recommend switching it in for rpi-gpio. https://github.com/fivdi/onoff#usage

        Good luck!

  • Ted Rust says:

    Also, I had way better luck using this method of controlling the garage, rather than the relay + direct-wiring it, which made my existing hardwired switch very unhappy.

    https://www.youtube.com/watch?v=uaBNv8_xlj4

    If you have a spare garage door remote hanging around, this can save a lot of complexity and a tiny bit of money. (Though it does add soldering skill requirements, so YMMV.)

  • Ben Solinski says:

    Thank you for this code! I plan on working on this project pretty soon.

    @Ted Rust – Would you be able to share your main.js file? I would like to implement the same code you did for not opening an already closed garage door. I am having a hard time figuring out which parts of the original code you are using and which you are replacing. Thank you in advance!

    • mxplea10 says:

      When the garage door is closed and/or closing), the iOS Home app Garage displays OPENING. And, when the garage door is opened and/or opening, the iOS Home app Garage displays CLOSING. It never displays CLOSE or OPEN. Has anyone else experienced a problem with the garage door status? If so, how was this issue corrected.

      @Ted Rust, @Ben Soinski
      Is this similar to the problem that you had? I don’t understate whether the code above is additional code or replacing code. Can you provide the specifics? I’d appreciate your help.

  • John Klever Batista says:

    I will pay 25$ to the one who give to me the SD files in a .rar file to use on an raspberry 3 you can contact my mail [email protected]

  • Clark S says:

    Hi, I am not javascript developer but I can usually follow instructions for getting things configured.

    It appears you left the HAP-Node installation and configuration as an exercise for the Student.

    Do you mind posting the main.js? Thanks!!

    • Michael says:

      Hello, have you completed this project or had your students complete it? If so, can you assist me with completing it? Thanks…

      • Michael says:

        Update: I’ve resolved the configuration problems and paired the camera and door. My next step is to test with the garage.

  • Matt says:

    Jordan – Love this series, especially the clean finish you did, with a proper case. How does Pi handle the environmental conditions of the garage? Do you have any issues in the heat of summer, or the cold of winter?

  • Zach says:

    Which folders are all these dependencies going into? If you ever get time, do you think maybe you could take a few better pictures of the wiring diagram? I’m just minutes away from pulling the trigger on all these supplies (but with a Pi0w). I’m super excited to start this weekend, but at the same time just a little apprehensive that the tutorial wouldn’t be basic enough in some areas. And don’t get me wrong, it is a great tutorial, but I’m relatively new to all this.

  • Michael says:

    I’m trying to emulate this project although it’s stated that some basic knowledge of JavaScript, Node, and Linux is required. I have limited knowledge of Linux and no programming knowledge of JavaScript and Node. I’d hope that I could still complete this project since the code has already been written and a clone is available. I purchased a Raspberry Pi 3 and installed Raspbian Stretch version 9. Jessie wasn’t available, at least I didn’t see it, on raspberrypi.org. After the intial update and upgrade, the steps taken are the following:

    homedir:~$ git clone –recursive https://github.com/jordancn/garage-pi.git

    homedir:~$ curl -sL https://deb.nodesource.com/setup_9.x | sudo bash –
    homedir:~$ sudo apt-get install -y nodejs

    ~/garage-pi $ npm install (generated errors)

    ~/garage-pi $ yarn controller (generated: bash: yarn: command not found)
    I haven’t been able to install yarn or find another program to start controller.

    I haven’t connected the Raspberry Pi to the garage door motor or sensor yet. I wanted to get the software installed and working if possible before testing the garage door and camera. The camera is connect to the Raspberry Pi already. If anyone can help me with this project and/or point me to helpful links, I would appreciate it.

    • Michael says:

      Just an update. I found out how to install the yarn package and just have to troubleshoot the error.

      ~/garage-pi $ yarn controller
      yarn run v1.3.2
      $ DEBUG=controller:* babel-node src/main.js
      module.js:487
      throw err;

      • Michael says:

        Update: I’ve resolved the configuration problems and paired the camera and door. My next step is to test with the garage.

        • Andreas Jetter says:

          it would be very kind, if you woud share how you fixed this.

      • duhmojo says:

        I must be confused. I have yarn but I keep getting “ERROR: There are no scenarios; must have at least one.” when I run yarn in the project directory. Yarn doesn’t work at all for me. “yarn controller” I assume should know to look at the package.json for the “controller” script. Any tips? Thanks.

        • duhmojo says:

          Ok, here’s how I managed to install the latest/working version of yarn. Raspian came with (I guess) version 0.27.

          sudo apt remove cmdtest
          sudo apt remove yarn

          Then following https://yarnpkg.com/lang/en/docs/install/ which really is just the following 3 lines:

          curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add –
          echo “deb https://dl.yarnpkg.com/debian/ stable main” | sudo tee /etc/apt/sources.list.d/yarn.list
          sudo apt-get update

          Then install yarn:

          sudo apt-get -o Dpkg::Options::=”–force-overwrite” install yarn

          yarn –version should give you >0.27, the latest for me is 1.3.2. Yarn will now work properly with the project. Hope this helps others.

        • Michael says:

          I got errors because node.js wasn’t installed. While researching the error I had, I read that sometimes the modules need to be removed and installed again before running the controller with yarn controller. I also did the remove and re-install when I made changes to the config.json file. If you need the specifics, let me know and I’ll check my notes. You might want to provide your email address.

      • eldiller says:

        HI Michael

        im getting a similar error

        yarn run v1.15.2
        $ DEBUG=controller:* babel-node src/main.js
        internal/modules/cjs/loader.js:583
        throw err;
        ^

        Error: Cannot find module ‘../build/Release/dns_sd_bindings’
        at Function.Module._resolveFilename (internal/modules/cjs/loader.js:581:15)
        at Function.Module._load (internal/modules/cjs/loader.js:507:25)
        at Module.require (internal/modules/cjs/loader.js:637:17)
        at require (internal/modules/cjs/helpers.js:22:18)
        at Object. (/home/pi/Garage/garage-pi/node_modules/mdns/lib/dns_sd.js:32:22)
        at Module._compile (internal/modules/cjs/loader.js:689:30)
        at Module._extensions..js (internal/modules/cjs/loader.js:700:10)
        at Object.require.extensions.(anonymous function) [as .js] (/home/pi/Garage/garage-pi/node_modules/babel-register/lib/node.js:152:7)
        at Module.load (internal/modules/cjs/loader.js:599:32)
        at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
        error Command failed with exit code 1.
        info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

        how did you fix your ???

  • Kyle says:

    Just an FYI:

    Part of the instructions are to download and install the latest version of Node.js. The latest version is 10.1-something. If you attempt to use this version, when attempting to install the project dependencies with npm/yarn, you will get a node-gyp rebuild error. Removing Node.js 10 and downgrading to the latest build of version 8 seemed to fix that error.

  • Bryan says:

    I was using “npm install” and “npm run controller” instead of the yarn package manager and running into an “import not found” error message. Had to do with something weird with the babel package. Anyhow, found the following site which allowed me to get pass this problem:

    https://timonweb.com/posts/how-to-enable-es6-imports-in-nodejs/

  • Comments are closed.