Skip to content

Using HAP NodeJS as a library

Ben edited this page Apr 1, 2023 · 11 revisions

Before reading this guide, you should already have read the wiki page about HomeKit Terminology.

In this guide, we are going to create a NodeJS module using HAP-NodeJS as a library to expose a simple HomeKit Light accessory.
It basically resembles the light example provided in the examples repository. So feel free to have a look at that as well, especially if you want an example written in Typescript, as this guide will only show an example written in JavaScript for the sake of a simpler project setup.

Toolchain setup

First, we need to set up our toolchain. For this, we will install Node.js and the Node Package Manager (npm) to manage all dependencies for our project. This guide does currently not provide any installation instructions for Node.js for every platform. A good starting point is to just navigate to the Node.js website. You may want to grab the Long-Term Support (LTS) version.
You may want to ensure that you install the minimum required node version required by HAP-NodeJS.

After that, node and npm should be installed on your computer. You can verify that by running the following commands. A similar output should be printed.

$ node -v
v13.13.0
$ npm -v
6.14.4

To write our code, you can use an IDE like Visual Studio Code, WebStorm or just a simple text editor like Atom or Notepad++.

Project setup

Now we want to create a new project by creating a new folder and opening your IDE in it. Some IDEs give prompts to create new projects and will open it automatically. In that case make sure you create an empty node.js project.
You may additionally want to open your project folder in the terminal.

We will create two files for our project: package.json and light.js.
Below are two sections where the complete content of each file is presented and then explained below line for line. So feel free to just copy the content, read through the explanations below and maybe follow additional instructions.

package.json

The package.json is a file describing your npm module and must be present in every project. It defines stuff like the entry point in your app or dependencies. For more details visit the official documentation for the package.json file.
It is written in JavaScript Object Notation.

{
  "name": "your-project-name",
  "version": "1.0.0",
  "description": "An example description for your example project",
  "main": "light.js",
  "author": "Example Name <[email protected]>",
  "license": "ISC",

  "dependencies": {
    "hap-nodejs": "latest"
  }
}

An explanation for the relevant fields used above:

  • name: this property sets the name of your module. It is used if you want to publish your package to the npm registry
  • version: this property is used to define the current version and should use Semantic Versioning
  • description: just a short descriptive text of your module
  • main: this is the main entry point for your module. This is where execution begins. We set it here to the light.js file we will create later.
  • author: in this property you should put in your name or username to mark your ownership
  • license: define the license the project is published with. For our examples the homebridge team uses the ISC license.
  • dependencies: this is one of the more important properties. Here we define dependencies of other npm modules. For our light example we only have a dependency of hap-nodejs with the version latest. Fell free to replace that with any specific version of hap-nodejs if you require that.

After we created that you want to open some sort of terminal and run the following command in the same directory as the package.json. This will install any dependencies (or dependencies of dependencies) required by your module.

npm install

light.js

light.js contains all code need to create our example light accessory.

const hap = require("hap-nodejs");

const Accessory = hap.Accessory;
const Characteristic = hap.Characteristic;
const CharacteristicEventTypes = hap.CharacteristicEventTypes;
const Service = hap.Service;

// optionally set a different storage location with code below
// hap.HAPStorage.setCustomStoragePath("...");

const accessoryUuid = hap.uuid.generate("hap.examples.light");
const accessory = new Accessory("Example Accessory Name", accessoryUuid);

const lightService = new Service.Lightbulb("Example Lightbulb");

let currentLightState = false; // on or off
let currentBrightnessLevel = 100;


// 'On' characteristic is required for the light service
const onCharacteristic = lightService.getCharacteristic(Characteristic.On);
// 'Brightness' characteristic is optional for the light service; 'getCharacteristic' will automatically add it to the service!
const brightnessCharacteristic = lightService.getCharacteristic(Characteristic.Brightness);


// with the 'on' function we can add event handlers for different events, mainly the 'get' and 'set' event
onCharacteristic.on(CharacteristicEventTypes.GET, callback => {
  console.log("Queried current light state: " + currentLightState);
  callback(undefined, currentLightState);
});
onCharacteristic.on(CharacteristicEventTypes.SET, (value, callback) => {
  console.log("Setting light state to: " + value);
  currentLightState = value;
  callback();
});


brightnessCharacteristic.on(CharacteristicEventTypes.GET, (callback) => {
  console.log("Queried current brightness level: " + currentBrightnessLevel);
  callback(undefined, currentBrightnessLevel);
});
brightnessCharacteristic.on(CharacteristicEventTypes.SET, (value, callback) => {
  console.log("Setting brightness level to: " + value);
  currentBrightnessLevel = value;
  callback();
});


accessory.addService(lightService); // adding the service to the accessory

// once everything is set up, we publish the accessory. Publish should always be the last step!
accessory.publish({
  username: "17:51:07:F4:BC:8A",
  pincode: "678-90-876",
  port: 47129,
  category: hap.Categories.LIGHTBULB, // value here defines the symbol shown in the pairing screen
});

console.log("Accessory setup finished!");

Before I explain the code in more detail you may want to just quickly test it and run it using the following command. The default pairing code is 678-90-876.

node light.js

Below we will discuss some parts of the code in more detail.


const hap = require("hap-nodejs");

const Accessory = hap.Accessory;
const Characteristic = hap.Characteristic;
const CharacteristicEventTypes = hap.CharacteristicEventTypes;
const Service = hap.Service;

The first few lines are all about importing the HAP-NodeJS library. It saves everything exported from the library into the variable named hap. The other variables are just there to make the code what follows cleaner. If we want to access the accessory class we do not need to always write hap.Accessory instead we will only write Accessory. We could give it a totally different name, but using the same name as defined by HAP-NodeJS improves code readability a lot.


const accessoryUuid = hap.uuid.generate("hap.examples.light");
const accessory = new Accessory("Example Accessory Name", accessoryUuid);

This two lines create a new accessory object from the Accessory class (saved into the accessory variable). The first argument passed is the name of the accessory displayed in the pairing screen, the second argument is the uuid.
One thing to point out is the first line, which generates a Universally Unique Identifier. In this example it is derived from the generator data "hap-examples.light". The same UUID will be generated for the same input. This UUID is used by HAP-NodeJS to uniquely identify an accessory and its related data stored on disk. So it must not change over the lifespan of an accessory, otherwise your configuration of the services exposed by this accessory may be reset in the Home app (like room assignments, automations and scenes).


const lightService = new Service.Lightbulb("Example Lightbulb");

let currentLightState = false; // on or off

The first line creates a new service, to be specific a Lightbulb service. The first argument is the name of the service (this is actually the name displayed in the tile in the Home app). All services supported by HAP-NodeJS can be accessed in that way Service.*.
A list of all services and characteristics can be found in ServiceDefinitions.ts and CharacteristicDefinitions.ts respectively. Just open them and search for Lighbulb and you will find the service definition of the Lighbulb service with all its required and optional characteristics, like the On characteristic (or the optional Brightness characteristic present in the code above).

We only cover the On characteristic in the detailed explanation, but you easily should be able to understand the code above for the Brightness characteristic as well, as it is pretty similar.

The second line is just a variable (a variable whose value can be changed, thus let and not const) where we store the current on/off state of the lightbulb.


const onCharacteristic = lightService.getCharacteristic(Characteristic.On);

// with the 'on' function we can add event handlers for different events, mainly the 'get' and 'set' event
onCharacteristic.on(CharacteristicEventTypes.GET, callback => {
  console.log("Queried current light state: " + currentLightState);
  callback(undefined, currentLightState);
});
onCharacteristic.on(CharacteristicEventTypes.SET, (value, callback) => {
  console.log("Setting light state to: " + value);
  currentLightState = value;
  callback();
});

This part is where the real magic is happening. First of all we are using calling the getCharacteristic method of our lightService object to get a reference to the characteristics object for the On characteristic (Characteristic.On).

Using the on method we then subscribe two event listeners, one for the 'get' event and one for the 'set' event.
The 'get' is called whenever a HomeKit controller request the current value of the characteristic. A callback is passed to the event handler as the first argument. The callback expects an Error object as the first argument if an error occurred or undefined as the first argument and the current value as the second argument to return the current value. The callback should be called as soon as possible.
The 'set' is called whenever a HomeKit controller sets a new value for the given characteristic. The newly set value is passed as the first argument. A callback is passed to the event handler as the second argument. The callback expects an Error object as the first argument if an error occurred.


accessory.addService(lightService); // adding the service to the accessory

After setting up our service we add it to our accessory by calling the addService method of the accessory.


// once everything is set up, we publish the accessory. Publish should always be the last step!
accessory.publish({
  username: "17:51:07:F4:BC:8A",
  pincode: "678-90-876",
  port: 47129,
  category: hap.Categories.LIGHTBULB, // value here defines the symbol shown in the pairing screen
});

After everything is set up, the last thing we will do is call the publish method of the accessory. The method takes a PublishInfo, basically an object full of options, as the first argument. The most important options are highlighted here:

  • username: this value is basically a mac address and must be in that format. It is used as an identifier of the HAP server by HomeKit. It can be any random mac address, but must be persistent over the lifespan of the accessory (as HAP-NodeJS even uses this to identify all stored files associated to this HAP server).
  • pincode: This is the eight digit pairing code. It must be specified in the exact same pattern as shown above.
  • port: This specifies the port the HAP (http) server is running on. Leave it out (or specify a value of 0) to use a random (free) port on start up.
  • category: This defines the category of the HAP server. It basically is just used to display the icon in the pairing menu.

So we basically covered now all important parts of the code. Let's start our accessory with the following command:

node light.js

Now you can navigate into the Home App and add the accessory to your Home with the provided setup code.

Horray you managed to add your first custom HomeKit accessory!

Building upon the example

To get you started with HAP-NodeJS development, try to modify the provided example.

As defined by the HomeKit Accessory Protocol (HAP) specification, the Lightbulb service has three additional optional characteristics: Hue and Saturation, to provide coloring functionality, or ColorTemperature, to change the temperature of a white bulb.

Try adding one of those characteristics and play around with it in the Home App.
Additionally, you can check out different types of services and their set of characteristics and explore how they react in the Home App.