App Development Guidelines

Using the Homey Apps SDK should be self-explanatory, yet some things might need some elaboration. This guide describes guidelines for app development on Homey. These guidelines ensure that apps and drivers are developed in a consistent way.

1. General

1.1. The Homey module

All available base classes reside in the Homey module. It should therefore be included in your App, Drivers and Devices in order to extend their respective base class. You can then access the managers through the this.homey property on these classes, for example:

Example /app.js

'use strict';

const Homey = require('homey');

class App extends Homey.App {
  onInit() {
    this.actionLock = this.homey.flow.getActionCard('lock');
    // ...
  }
}

module.exports = App;

In api.js the homey instance is passed as an argument to your route handler:

Example /api.js

module.exports = {
  async getSomething({ homey }) {
    return await homey.app.getSomething();
  },
};

Read more about api.js in the Web API.

1.2. SDK Version

The current SDK version is 3, and you must define this in your /app.json.

Not doing so will result in your app falling back to SDK level 2, which is deprecated and won't be accepted in the App Store.

Example /app.json

{
  "id": "com.athom.example",
  "sdk": 3
}

1.3. Code style

Try to keep a consistent code style in your apps. For this we recommend to use ESLint with a predefined eslint config (check out our own: eslint-config-athom). You can manually run eslint --fix . in your app directory, integrate it in your favorite IDE, or attach it to a key shortcut so that you can create a habit out of formatting your code easily.

2. Drivers

Drivers are used to add support for devices of a certain type or brand to Homey. It is important to create drivers of high quality which are consistent so that users can enjoy the best experience possible. Each device type has its own challenges and opportunities which are addressed below.

2.1. Battery devices

A device which can report its battery level can do so in two ways:

  1. Report when the battery is empty or low.
  2. Report the precise battery level that is left.

Give your driver the measure_battery capability if it supports reporting precise battery levels on a certain numeric scale (e.g. 0-100%). Give your driver the alarm_battery capability if it supports alarm notifications when the battery level reaches a certain threshold (e.g. 'battery level less than 10%).

Never give your driver both the measure_battery and the alarm_battery capabilities. This creates duplicate UI components and Flow cards.

Battery devices must specify an energy object with the batteries property. This should be set to an array of strings which represent the batteries in the device. For example, a device with 2 AAA batteries must specify the following energy object:

Example /app.json

{
  "id": "com.athom.example",
  "drivers": [
    {
      "id": "my_driver",
      "name": {
        "en": "My Driver"
      },
      "capabilities": ["measure_battery"],
      "energy": {
         "batteries": ["AAA", "AAA"]
      },
      // ...
    }
  ],
  // ...
}

For more information check the Energy documentation.

2.2. Lights

Lights are devices that are light sources itself or devices that can directly control light sources. This entails smart light bulbs (e.g. LIFX), sockets (e.g. Fibaro Wall Plug) and (flush) modules (e.g. Fibaro Dimmer 2). Sockets can be seen as a different category, however sockets that have the onoff and dim capabilities are most often used for lights and are therefore relevant to consider in this category.

2.2.1. Choosing a device class

In general light devices have the light device class. In some cases socket is more applicable. A socket device has a “What’s plugged in?” setting that allows a user to choose a virtual device class (e.g. light). Obviously, for socket devices such as Fibaro Wall Plug and Qubino Plug socket is the required device class.

For (flush) modules, such as the Qubino Flush Dimmer, a tradeoff has to be made. If there are use cases for the device other than controlling a light source the required device class should be socket. The Qubino Flush Dimmer 0 - 10V has various use cases other than controlling light sources, for example bathroom ventilation control. When in doubt, use socket since this allows users to configure the device as light (or something else) after pairing.

Although socket allows the user to choose “what’s plugged in” it is required to give a device the class light when possible. This provides a better user experience since it will have the right UI components by default. Otherwise the user needs to find the “what’s plugged in” setting to correct it.

2.2.2. Capabilities and behaviour

Implementing capabilities of lights needs some consideration. We start with the two basic capabilities onoff and dim.

2.2.2.1. Capabilities onoff and dim

A light device can be turned on and off via the toggle UI component (see left image below) as well as the dim level UI component (see right image below). When the device is off, dragging the dim slider from 0 to non-zero must turn the device on (this means the onoff capability needs to be set to true). When the device is on, dragging the dim slider from non-zero to 0 must turn the device off (this means the onoff capability needs to be set to false).

If the device is using a technology that allows it to report its onoff and dim state to Homey when it is changed through external inputs (such as connected switches) this should be reflected in the device UI. If the device is turned off externally, the device should be turned off in Homey, same for dimming. Zigbee and Z-Wave are examples of technologies that almost always support this.

The onoff and dim capabilities should be coupled together and debounced. This can be done using the registerMultipleCapabilityListener method. This is required because users can create Flows with both an onoff and dim action. In order to prevent duplicate (potentially conflicting) commands being sent to a device (e.g. turn on and dim to 50% which would result in a device turning on to the last known dim value and then dimming back to 50%) these capabilities need to be debounced together and combined into one command to the device.

In the unexpected case that a user creates a conflicting Flow, such as turn on and dim to 0%, or turn off and dim to 50%, make sure the onoff capability is leading. That means, if the dim value is zero, but the new onoff value is true, turn the light on and disregard the dim value and vice versa.

Example /drivers/my_driver/device.js

this.registerMultipleCapabilityListener(['onoff', 'dim'], ({ onoff, dim }) => {
  if (dim > 0 && onoff
  false) {
    return this.api.setOnOffAsync(false); // turn off
  }
  else if (dim <= 0 && onoff === true) {
    return this.api.setOnOffAsync(true); // turn on
  }
  return this.api.setOnOffAndDimAsync({ onoff, dim }); // turn on or off and set dim level in one command
});

Using the setOnDim capability option for the onoff capability will result in Homey sending only a dim capability set instead of both onoff and dim when a user or Flow changes the dim level of a device.

2.2.2.2. Color and temperature capabilities

Light devices supporting one or multiple of the following capabilities are a little bit more complex:

  • light_hue
  • light_saturation
  • light_temperature
  • light_mode

A few things are important in this case.

Similar to onoff and dim the color and temperature capabilities need to be grouped and debounced to prevent flickering of the device. In general it is best to debounce the color and temperature capabilities together with onoff and dim, but this might depend on the technology you are using and how the device responds to different commands. The general rule is to prevent flickering (due to multiple commands being sent from one Flow or user action) as much as possible.

When the device is turned off, changing the light_hue, light_saturation, light_temperature or light_mode capabilities must not turn the device on. Only onoff and dim are allowed to change the on/off state of a device. In the case that the user turns the device on through external inputs (e.g. connected switches) the driver should listen (or actively request if needed) for updates with regard to the light_hue, light_saturation, light_temperature or light_mode capabilities and reflect the actual state of the device in the UI.

In the unexpected case that a user creates a conflicting Flow, such as turn off and set color to red, make sure the onoff capability is leading. That means, turn off the light even though the user changed the color to red.

Example /drivers/my_driver/device.js

const lightCapabilities = [
  'onoff',
  'dim',
  'light_hue',
  'light_temperature',
  'light_saturation',
  'light_mode',
];
this.registerMultipleCapabilityListener(
  lightCapabilities,
  ({ onoff, dim, light_hue, light_temperature, light_saturation, light_mode }) => {
    // handle the changed capabilities here all at once
    return this.api.setOnOffAndDimAndColorAsync();
  },
);

2.3. Window coverings

A window coverings device should only be assigned the window_coverings device class if one of the following is not more applicable: curtains, blinds, sunshade.

There are a number of capabilities relevant for window coverings devices:

  • windowcoverings_state
  • windowcoverings_tilt_up
  • windowcoverings_tilt_down
  • windowcoverings_tilt_set
  • windowcoverings_closed
  • windowcoverings_set

In general there are two types of window coverings which should receive a subset of these capabilities:

2.3.1. Type 1: window coverings that can only be controlled with up/down and stop commands

This type should only implement the windowcoverings_state capability and in case the device supports horizontal tilt of venetian blinds also the windowcoverings_tilt_up and windowcoverings_tilt_down capabilities.

2.3.2. Type 2: window coverings that can be controlled by sending a command with a precise open/close level

This type should only implement the windowcoverings_set capability and in case the device supports horizontal tilt of venetian blinds also the windowcoverings_tilt_up and windowcoverings_tilt_down capabilities.

Never implement both windowcoverings_state and windowcoverings_set for one driver. This creates duplicate UI components and Flows cards.

3. Command line interface

The Getting Started guide demonstrates how to get started with the homey command line interface. There are many more features that it has to offer. There are multiple plugins created for homey which are very useful, for more information check the documentation.

If you are creating large apps, with many drivers, it is advised to use the compose plugin which allows you to add more structure and remove redundancy from the app while improving development efficiency and consistency. For Z-Wave and Zigbee apps homey-zwavedriver and homey-zigbeedriver were created, similarly for RF apps there is homey-rfdriver. All apps can take benefit of homey-log which integrates Sentry (a remote error tracking tool) into your app.

4. Publishing your app

Well done, you have an app that you think is ready to be published! But before you do check out the App Store guidelines, to make your app review process go smoothly.

4.1. How to publish?

It is as easy as running $ homey app publish from your app directory.

Your app will be compressed and send to the Homey App Store for processing. Go to https://developer.athom.com, tap Apps SDK and choose My Apps. All your apps are visible here, and you can publish your app to Test, or Live by submitting it for certification. Once your app is approved by a reviewer at Athom it can be published.

By default your app will be submitted as Draft. You can then choose to release a Test version of the app, only available for users who visit your app via the Test link (available in your dashboard). After some proper testing the app can be submitted for certification by Athom. After approval it will be published to the Homey Apps Store and becomes available to all Homey users.

Apps that have never been reviewed cannot be tested by external users.

4.2. Requirements

To check if all requirements are met, run the following command prior to submission.

$ homey app validate --level publish

5. Updating your app

There comes a point in time when you want to release an update to your app to the Homey Apps Store. There are some things to consider.

5.1. What's new?

Updating your app with some fixes or new features? Awesome! Let your users know what’s new and what has changed for them. When publishing a new version of your app a .homeychangelog.json file is automatically created in the root directory of your app and homey will ask you 'What's new?'. Here you can list all the new features, content or functional changes since the previous live version.

What do your users want to see?

  • Only name the changes that are relevant to your users: _ Added for new features. _ Changed for changes in existing functionality. _ Deprecated for soon-to-be removed features. _ Removed for (temporary) removed features. _ Fixed for any bug fixes. _ Security in case of vulnerabilities.
  • In case you've made some changes to for example your readme based on our feedback there is no need to mention this in your changelog. Only changes that have been made since the previous live version are important.

5.2. Migration strategies and breaking changes

In the case that you need to make a change to your app that might break functionality for current users, be very careful.

Situations that might result in breaking existing functionality for users:

  • Removing or adding driver capabilities
  • Changing or removing Flow cards
  • Changing a driver's device class

Publishing breaking changes is in general not allowed. Homey users expect that their devices and Flows will continue to work after receiving app updates. As a developer you can make sure that users never have to experience breaking changes. Almost all users have automatic app updates enabled, this gives you great responsibility as an app developer!

5.2.1. Adding or removing capabilities

You might want to update your drivers with new capabilities at some point. Adding new or improved functionality is great. Maybe you also want to remove some old capabilities which are no longer useful. This can be done programmatically by calling removeCapability. Be careful, if there are Flow cards related to this capability they will be removed and existing Flows will break.

Example /drivers/my_driver/device.js

class MyDevice extends Homey.Device {
  onInit() {
    // Check yourself if migration is needed, do not call these methods on every init!
    if (!this.isMigrated()) {
      this.removeCapability('windowcoverings_state').then(this.log).catch(this.error);

      this.addCapability('windowcoverings_set').then(this.log).catch(this.error);
    }
  }
}

In most cases, it is best to only remove the capability from the driver object in app.json. This makes sure that for already paired devices nothing will break, UI components and Flow cards will remain working. However, the removed capability will be unavailable when a new device is paired. When applying this migration strategy, it is important that you do not remove the capability listener for the removed capability in the future, this will break functionality for already paired devices.

5.2.2. Deprecating Flow cards

When you want to remove a Flow card from your app, or change how it is constructed (e.g. add or remove arguments) you should add the "deprecated": true flag. This will ensure that users who create new Flows will not have the option to select this Flow card anymore, but users who still have this Flow card as part of their Flows will not experience breaking Flows. When this Flow card is deprecated you can create a new Flow card with the updated functionality that replaces the deprecated one. Make sure to not remove or change the Flow card handler in your code, this would still break existing Flows.

Example /app.json

{
  "flow": {
    "actions": [
      {
        "id": "flow_action",
        "deprecated": true,
        "title": {
          "en": "Flow Action Title"
        }
      }
    ]
  }
}

5.2.3. Changing the device class

If you need to change the device class of your device, the setClass method is available to do so. Please note, some Flow cards might depend on a certain device class, changing it will result in broken Flows for users in that case.

Example /drivers/my_driver/device.js

class MyDevice extends Homey.Device {
  onInit() {
    // Check yourself if migration is needed, do not call this method on every init!
    if (!this.isMigrated()) {
      this.setClass('light').then(this.log).catch(this.error);
    }
  }
}

5.2.4 Deprecating drivers

If all the above does not result in a migration strategy that does not break existing functionality of your driver for current users, it might be best to deprecate the driver as a whole. This can be done by adding the "deprecated": true flag to your driver manifest.

Example /app.json

{
  "id": "com.athom.example",
  "drivers": [
    {
      "id": "my_driver",
      "name": {
        "en": "My Driver"
      },
      "deprecated": true,
      "capabilities": ["onoff", "dim"],
      // ...
    }
  ],
  // ...
}