Repository navigation
Using HAP NodeJS as a library
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.
Please note that the examples repository still uses a very old version (hap-nodejs v0.6) and the older callback style.
So some package names and methods there are different from this guide.
The examples in this guide are written for HAP-NodeJS v2.3.0 or newer, which is published on npm as
@homebridge/hap-nodejs.
The old hap-nodejs package (without the @homebridge/ part) only has v1 and older, so don't use it for new projects.
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.
Make sure you install a Node.js version that HAP-NodeJS supports:
-
v22.12.0or a newerv22release - any
v24release - any
v26release
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
v26.13.0
$ npm -v
11.20.0
To write our code, you can use an IDE like Visual Studio Code, WebStorm or just a simple text editor like Notepad++.
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.
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",
"type": "module",
"main": "light.js",
"author": "Example Name <mail@example.com>",
"license": "ISC",
"dependencies": {
"@homebridge/hap-nodejs": "^2.3.0"
}
}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 -
type: setting this tomoduletells Node.js that our.jsfiles are ES modules. This lets us useimportandawaitat the top of our file. -
main: this is the main entry point for your module. This is where execution begins. We set it here to thelight.jsfile 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@homebridge/hap-nodejs. The version^2.3.0means "version 2.3.0, or any newer version below 3.0.0". Runningnpm install @homebridge/hap-nodejsin your project folder adds this line for you, with the newest version.
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 contains all code need to create our example light accessory.
import hap from '@homebridge/hap-nodejs'
const { Accessory, Categories, Characteristic, Service } = hap
// optionally set a different storage location with the code below (it must be an absolute path)
// hap.HAPStorage.setCustomStoragePath('/absolute/path/to/storage')
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 'onGet' and 'onSet' we add handlers for reading and writing the value
onCharacteristic.onGet(() => {
console.log(`Queried current light state: ${currentLightState}`)
return currentLightState
})
onCharacteristic.onSet((value) => {
console.log(`Setting light state to: ${value}`)
currentLightState = value
})
brightnessCharacteristic.onGet(() => {
console.log(`Queried current brightness level: ${currentBrightnessLevel}`)
return currentBrightnessLevel
})
brightnessCharacteristic.onSet((value) => {
console.log(`Setting brightness level to: ${value}`)
currentBrightnessLevel = value
})
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!
await accessory.publish({
username: '17:51:07:F4:BC:8A',
pincode: '678-90-876',
port: 47129,
category: 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.
import hap from '@homebridge/hap-nodejs'
const { Accessory, Categories, Characteristic, Service } = hapThe 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.
You can also import only the parts you need, for example import { Accessory, Service } from '@homebridge/hap-nodejs'.
If your project uses CommonJS (require) instead of ES modules, const hap = require('@homebridge/hap-nodejs') also works.
This needs Node.js v22.12.0 or newer. In that case, use .then() instead of await around the publish call at the end,
because CommonJS files can't use await at the top level.
The commented-out line below the imports shows how to store the pairing data in a different folder.
By default, HAP-NodeJS stores it in a folder called persist, inside the folder you started your app from.
If you set your own folder, it must be an absolute path (a full path, like /home/pi/light-storage).
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 offThe 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 Lightbulb and you will find the service definition of the Lightbulb service with all its required and
optional characteristics, like the On characteristic (or the optional Brightness characteristic present in the code above).
You can also browse them in the API documentation.
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 'onGet' and 'onSet' we add handlers for reading and writing the value
onCharacteristic.onGet(() => {
console.log(`Queried current light state: ${currentLightState}`)
return currentLightState
})
onCharacteristic.onSet((value) => {
console.log(`Setting light state to: ${value}`)
currentLightState = value
})This part is where the real magic is happening. First of all we are calling the getCharacteristic method of
our lightService object to get a reference to the characteristics object for the On characteristic (Characteristic.On).
We then add two handlers, one with onGet and one with onSet.
The onGet handler is called whenever a HomeKit controller requests the current value of the characteristic.
It just returns the current value. It should return as soon as possible.
The onSet handler is called whenever a HomeKit controller sets a new value for the given characteristic.
The newly set value is passed as the first argument.
Both handlers may also be async functions (or return a promise), if you need to wait for your device.
To report a problem, throw a HapStatusError, for example
throw new hap.HapStatusError(hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE).
Before you do that, read Presenting Erroneous Accessory State to the User.
You may see older code use .on('get', callback) and .on('set', callback) instead.
That older callback style still works, but onGet and onSet are simpler and are the recommended way.
accessory.addService(lightService) // adding the service to the accessoryAfter 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!
await accessory.publish({
username: '17:51:07:F4:BC:8A',
pincode: '678-90-876',
port: 47129,
category: 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.
publish returns a promise, so we await it before we print our "finished" message.
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 (asHAP-NodeJSeven 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 of0) 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. -
advertiser: This picks the library used to announce the accessory on your network (mDNS). If you leave it out, HAP-NodeJS usesMDNSAdvertiser.CIAO. The other choices areMDNSAdvertiser.BONJOUR,MDNSAdvertiser.AVAHIandMDNSAdvertiser.RESOLVED. The Homebridge wiki explains the differences between them.
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.
Hooray, you managed to add your first custom HomeKit accessory!
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 more
optional characteristics. For example: 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.
- Raspberry Pi
- Debian, Ubuntu
- CentOS, Fedora, Red Hat
- Arch, Manjaro
- Docker
- Virtual Machine (Windows & macOS)
- Windows
- macOS
- Synology DSM 7
- Other Platforms
- Basic Troubleshooting
- Backup and Restore
- Child Bridges
- Config File
- Connect To HomeKit
- FFmpeg for Homebridge
- HomeKit Glossary of Terms
- iOS Apps
- mDNS Options
- Remote Access
- Useful Links
- Basic Troubleshooting
- Config Options
- Enabling Accessory Control
- Enabling UI with Docker
- Homebridge Service Command
- Manual Configuration
- Reverse Proxy: Apache
- Reverse Proxy: Nginx and SSL
- Manage Homebridge Version
- Manage Homebridge UI Version
- Manage Plugin Version
- Manage Node Version
- Updating To Homebridge v2
Raspberry Pi Image
- Pi Image Home
- Getting Started
- Supported Raspberry Pi Models
- Find the IP Address
- Connect Via SSH
- Connect Via VNC (old)
- Set the Date and Time
- SSL / HTTPS Access
- Disable the WiFi Setup Portal
- Reset WiFi
- Upgrade to the Latest Image
- Update Homebridge and Node.js
- Enable Debug or Insecure Mode
- Install Pi-hole
- Install AdGuard Home
- Install deCONZ (ConBee / RaspBee)
- Migrate From HOOBS
- Fix Eero Router Connectivity
Docker
Synology Package
HAP-NodeJS
- Developer Docs
- API Reference
- Plugin Templates
- Other Links (Internal)
- Other Links (External)