Bluetooth LE

Bluetooth Low Energy is a wireless communication standard with reduced power consumption compared to classic Bluetooth. Bluetooth Low Energy uses the 2.4GHz radio frequency.

Bluetooth devices define a table of data called the Generic Attribute profile, also called GATT. This table consist of hierarchy of the following types: Advertisements -> Peripheral -> Service -> Characteristic -> Descriptor.

Advertisements allow Bluetooth devices to discover each other by broadcasting messages, these can be received without having to pair the devices. Advertisements have a one-to-one relation with Peripherals, once a Peripheral has been discovered it can be connected to. After connecting the paired devices expose their Services to each other. Services contain one or more Characteristics, and Characteristics contain zero or more Descriptors. Characteristics usually represent a specific state of the device, for example a Bluetooth thermostat may have Characteristics for temperature and humidity. Descriptors contain the metadata associated with the Characteristic. Often there is a "Characteristic User Description" Descriptor that provides more details on the meaning of these values in the descriptor, for example that a thermometer is measuring the temperature in Celsius.

In order to use BLE in your app will need the homey:wireless:ble permission. For more information about permissions read the permissions guide.

You can view a working example of a Homey App that uses BLE at: https://github.com/athombv/com.mipow-example

Device discovery

One of the first things to do when using Bluetooth on Homey is to perform a device discovery. Using the ManagerBLE#discover() function we can detect advertisements of devices near Homey. An advertisement contains some generic information from the device, and contains the data that is necessary to be able to connect to the device as well.

const advertisements = await this.homey.ble.discover();

How a BLE devices exposes itself to Homey is entirely up to the manufacturer of the device, therefore it may be possible that devices expose themselves in different ways.

Setting a custom timeout for a BLE discovery has been removed in Homey v6.0.0. Instead, a default timeout of 5 seconds is being used.

Performing a discovery will return an array of advertisements. Each advertisement package contains some data about the peripheral. (Note that not all data may be present depending on the discovered device).

Key

Description

Always present?

uuid: string

The ID of the peripheral.

Yes

rssi: number

The signal strength.

Yes

localName: string

The name of the peripheral.

No

connectable: boolean

Whether the device can be connected to or not.

Yes

serviceUuids: string[]

Some peripherals show one or more services in their advertisement.

This list does not necessarily contain all services of this peripheral.

No

state: string

The state of the peripheral ("(dis)connected", "(dis)connecting", "error")

Yes

address: string

The mac address of the peripheral

Yes

addressType: string

The type of the address

Yes

serviceData: {}

Some data a peripheral may expose during advertisement

No

Service Filtering

It is possible to pass a service filter to the discovery function. The example below will return only the advertisements that have the given service UUID exposed in their advertisement.

const advertisements = await this.homey.ble.discover(
['0000180000001000800000805F9B34FB']
);

Finding devices by UUID using find()

An alternative to the discover() function is the ManagerBLE#find() function. When you already know the UUID of the peripheral you want to connect with you can get the advertisement of the peripheral like this:

const advertisement = await this.homey.ble.find('my_device_id');

If the device already has been discovered by Homey you will receive the most recent advertisement instantaneously. If Homey does not yet know the device, then Homey will perform a discovery for you first.

Connecting and Disconnecting devices

Creating a connection to a device can be done by calling BleAdvertisement#connect().

const peripheral = await advertisement.connect();

It is always possible that this function rejects. For example when another app is connected using the peripheral, or when the peripheral is not available anymore. If the peripheral is already connected then you will receive the existing connection.

Some devices do not support multiple BLE connections. This means that a permanent connection with Homey would block other devices from connecting. Please do not keep a connection with a device occupied if there is no reason to do so.

Disconnecting from a device is very trivial. If the device is already disconnected the call to BlePeripheral#disconnect() will simply resolve. If, for some reason, you have opened multiple connections to one device Homey will only actually close the BLE connection when all connections are closed.

await peripheral.disconnect();

Starting from Homey 6.0 peripherals do not automatically disconnect anymore after 60 seconds of inactivity. Some devices work better when an active connection is maintained.

Listening to disconnects

When you maintain an active connection it is possible to listen for disconnect events. Homey will emit this event when it detects that a device is not connected anymore. When a BLE device is connected it is allowed for this device to turn off the radio while maintaining a connection with Homey. This is a feature of BLE to save energy. Usually, when the device has new data it may wake up and notify Homey of this new data.

If during this period the device is turned off, or brought out of the range of Homey, it will still be registered in Homey as connected. For BLE this is correct behaviour: if the device comes back into range of Homey then the connection may be restored. However, if the external device has lost the connection (for example by being turned off) then Homey will see that this device has "disconnected" when it is in reach again. It is at this point that the disconnect event will be emitted.

Reading and writing data

After a connection to a peripheral has been established it is possible to read and write data to the device. The easiest way to do this is using the two shorthand functions: BlePeripheral#read() and BlePeripheral#write(). However, if more flexibility is desired it can also be achieved by accessing a characteristic immediately.

Using peripheral shorthands

The advantage of using these shorthands is that you do not need to worry about service- and characteristic discovery: this will be done for you.

// Reading
const data = await peripheral.read(serviceUuid, characteristicUuid);
// Writing
await peripheral.write(serviceUuid, characteristicUuid, data);

Using a Characteristic

Reading and writing data can also be done from a characteristic. Using this approach requires you to discover Services and Characteristics first.

// Reading
const data = await characteristic.read();
// Writing
await characteristic.write(data);

Using a Handle

Reading and writing data using a Handle is not supported.

Bluetooth Notifications

Starting with Homey 6.0 it is possible to use BLE notifications. In your app you can register for BLE notifications using the BleCharacteristic#subscribeToNotifications() function. This function expects a callback which is called whenever a notification is received. The received data from the notification is available in the `data` argument.

Subscribing

const data = await characteristic.subscribeToNotifications(data => {
console.log('Received notification: ', data);
});

Unsubscribing

If you desire to stop listening to notifications it is possible to unsubscribe.

await characteristic.unsubscribeFromNotifications();

Service Discovery

Discover all services from a peripheral

const services = await peripheral.discoverServices();

Discover a single service

const service = await peripheral.getService(serviceUuid);

Included Services

Not (yet) supported

Characteristic Discovery

const characteristics = await service.discoverCharacteristics();

Creating a BLE app

In this section a guide is provided to get started with a new Bluetooth Low Energy app.

The first thing to do is to perform some exploration on how to interact with this device. For this you can use the BLE Developer Tool.

Key

value

Device Name

my_device_name

Service UUID

my_service_uuid

Characteristic UUID

my_characteristic_uuid

Format of received data

[temp1, temp2]

Getting started

When you have all data necessary to interact with your device it is time to start building. Use homey app create using the Homey CLI to get started with your new app.

Since we are creating a device, app.js can stay as it is. Instead, we must add a new driver.

Make sure you have read about Drivers and Devices before you continue

Creating a BLE driver

To create a new driver, run homey app driver create and configure it to your liking. The driver is responsible for finding and pairing your BLE device. In other words, when a user tries to add your BLE device in the app, then this user will be using your driver to scan for the device and to pair with it.

This means that a scan must be performed to find all BLE devices near Homey, and then show the device that our app will support. This means that we will have to do a few things:

  • Scan for BLE devices when a user wants to pair a device

  • Filter the BLE devices for a specific property in the advertisement (for example an exposed service or the name of the device).

  • Format this data such that Homey can show it to the user.

The tree steps above are implemented in this example:

/driver/<driver_id>/driver.js
const Homey = require('homey');
class Driver extends Homey.Driver {
async onPairListDevices() {
const advertisements = await this.homey.ble.discover();
return advertisements
.filter(advertisement => advertisement.localName === 'my_device_name')
.map(advertisement => {
return {
name: advertisement.localName,
data: {
id: advertisement.uuid,
},
store: {
peripheralUuid: advertisement.uuid,
}
};
});
}
}
module.exports = Driver;

Please note that in the map() function a 'store' property is added. This property will be used in the next step to identify our device using the ManagerBLE#find() method.

Creating a device

Now that a Driver has been created it is possible for a user to pair the device to your app. When you have paired your BLE device, the device should be visible in the "devices" view in the Homey Mobile App or the Homey Web App. In the debug logging for your app you can see that the driver and the device both have been initialized.

Advertisements and Connections

The example below shows a simple implementation for receiving the BLE representation of your device within your app. When your Homey Device is being initialized an interval is started that checks for the existence of the desired advertisement. Your device will be available as this.advertisement.

This code can easily be extended to provide an 'always-connected'-approach, which may be useful in some cases, for example lights where latency needs to be minimized. Be careful to use approach this since many BLE devices only support one connection, which means you may block other devices from connecting to your BLE device.

/driver/<driver_id>/device.js
const Homey = require('homey');
const RETRY_INTERVAL = 10 * 60 * 1000; // 10 minutes
class Device extends Homey.Device {
async onInit() {
this._interval = this.homey.setInterval(() => {
this.scan();
}, RETRY_INTERVAL);
}
async scan() {
// Get the advertisement - we only need to get it once
if (!this.advertisement) {
this.advertisement = await this.homey.ble.find(this.getStore().peripheralUuid);
}
// Add this block if you wish to have a permanent connection.
if (this.advertisement && !this.peripheral) {
this.peripheral = advertisement.connect();
this.peripheral.once('disconnect', () => {
this.peripheral = null;
});
}
}
}
module.exports = Device;

Writing and disconnecting

When you have your advertisement available in your app, for example as this.advertisement, a lot of things are possible. For example writing to a peripheral:

this.registerCapabilityListener('my_capability', () => {
const peripheral = await this.advertisement.connect();
await peripheral.write('my_service', 'my_characteristic', data);
await peripheral.disconnect();
}

Or, in case of a permanent connection through this.peripheral:

this.registerCapabilityListener('my_capability', () => {
await this.peripheral.write('my_service', 'my_characteristic', data);
}

Accessing a Service/Characteristic

A basic implementation for BLE notifications could be the following:

// Find the service
const services = await this._connection.discoverServices();
const dataService = services.find(service => service.uuid === 'my_service_uuid');
if (!dataService) throw new Error('Could not find service');
// Find the characteristic
const dataCharacteristics = await dataService.discoverCharacteristics(['my_characteristic_uuid']);
if (!dataCharacteristics || !dataCharacteristics.length) throw new Error('Could not find Characteristic');
this._dataCharacteristic = dataCharacteristics[0];